From 288f6d1134e790e47f7650ddc9f026b18a3059e5 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 21:39:11 -0500 Subject: [PATCH 01/54] Scaffold Text-Grab.Core, Text-Grab.Core.Windows, and Tests.Core projects Phase 0 of the v5 workflow engine roadmap: adds the class-library split (pure Core, Windows-TFM Core.Windows) and a fast pure-unit-test project that later phases will move logic into, with no production code moved yet. Both new libraries needed explicit RuntimeIdentifiers matching the app so the MSIX wapproj restores project references per-RID. --- Tests.Core/ScaffoldingSmokeTests.cs | 10 +++ Tests.Core/Tests.Core.csproj | 20 ++++++ Tests.Core/Usings.cs | 1 + .../Text-Grab.Core.Windows.csproj | 19 +++++ Text-Grab.Core/Text-Grab.Core.csproj | 15 ++++ Text-Grab.sln | 72 +++++++++++++++++++ Text-Grab/Text-Grab.csproj | 5 ++ 7 files changed, 142 insertions(+) create mode 100644 Tests.Core/ScaffoldingSmokeTests.cs create mode 100644 Tests.Core/Tests.Core.csproj create mode 100644 Tests.Core/Usings.cs create mode 100644 Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj create mode 100644 Text-Grab.Core/Text-Grab.Core.csproj diff --git a/Tests.Core/ScaffoldingSmokeTests.cs b/Tests.Core/ScaffoldingSmokeTests.cs new file mode 100644 index 00000000..840df60c --- /dev/null +++ b/Tests.Core/ScaffoldingSmokeTests.cs @@ -0,0 +1,10 @@ +namespace Text_Grab.Tests.Core; + +public class ScaffoldingSmokeTests +{ + [Fact] + public void TestsCoreProject_RunsAgainstTextGrabCore() + { + Assert.True(true); + } +} diff --git a/Tests.Core/Tests.Core.csproj b/Tests.Core/Tests.Core.csproj new file mode 100644 index 00000000..672e068a --- /dev/null +++ b/Tests.Core/Tests.Core.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + Exe + Text_Grab.Tests.Core + enable + enable + false + + + + + + + + + + + diff --git a/Tests.Core/Usings.cs b/Tests.Core/Usings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/Tests.Core/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj new file mode 100644 index 00000000..acc77374 --- /dev/null +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -0,0 +1,19 @@ + + + + net10.0-windows10.0.22621.0 + 10.0.22621.48 + Text_Grab + enable + enable + false + false + false + win-x86;win-x64;win-arm64 + + + + + + + diff --git a/Text-Grab.Core/Text-Grab.Core.csproj b/Text-Grab.Core/Text-Grab.Core.csproj new file mode 100644 index 00000000..33c5ae14 --- /dev/null +++ b/Text-Grab.Core/Text-Grab.Core.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + Text_Grab + enable + enable + false + + win-x86;win-x64;win-arm64 + + + diff --git a/Text-Grab.sln b/Text-Grab.sln index 4c6992d1..26d05451 100644 --- a/Text-Grab.sln +++ b/Text-Grab.sln @@ -16,14 +16,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextGrab.AutomationHost", "UiTests\TextGrab.AutomationHost\TextGrab.AutomationHost.csproj", "{51D9D3FA-2722-4203-AE21-AEA1B11C761D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Text-Grab.Core", "Text-Grab.Core\Text-Grab.Core.csproj", "{DFFADAEC-2E5A-4F14-B649-2E006558ACA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Text-Grab.Core.Windows", "Text-Grab.Core.Windows\Text-Grab.Core.Windows.csproj", "{F84E41ED-2A83-4717-99F8-734C7A34A690}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Core", "Tests.Core\Tests.Core.csproj", "{F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 + Debug|Any CPU = Debug|Any CPU Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -32,12 +40,16 @@ Global {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Debug|x64.Build.0 = Debug|x64 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Debug|x86.ActiveCfg = Debug|x86 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Debug|x86.Build.0 = Debug|x86 + {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|ARM64.ActiveCfg = Release|ARM64 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|ARM64.Build.0 = Release|ARM64 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|x64.ActiveCfg = Release|x64 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|x64.Build.0 = Release|x64 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|x86.ActiveCfg = Release|x86 {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|x86.Build.0 = Release|x86 + {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE1A1C35-BF6A-4141-BD91-260E0D2794BA}.Release|Any CPU.Build.0 = Release|Any CPU {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|ARM64.Build.0 = Debug|ARM64 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|ARM64.Deploy.0 = Debug|ARM64 @@ -47,6 +59,8 @@ Global {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|x86.ActiveCfg = Debug|x86 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|x86.Build.0 = Debug|x86 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|x86.Deploy.0 = Debug|x86 + {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE37F469-F629-4B49-84BA-37A1DA192C60}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|ARM64.ActiveCfg = Release|ARM64 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|ARM64.Build.0 = Release|ARM64 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|ARM64.Deploy.0 = Release|ARM64 @@ -56,30 +70,88 @@ Global {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|x86.ActiveCfg = Release|x86 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|x86.Build.0 = Release|x86 {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|x86.Deploy.0 = Release|x86 + {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE37F469-F629-4B49-84BA-37A1DA192C60}.Release|Any CPU.Build.0 = Release|Any CPU {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|ARM64.ActiveCfg = Debug|ARM64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|ARM64.Build.0 = Debug|ARM64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|x64.ActiveCfg = Debug|x64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|x64.Build.0 = Debug|x64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|x86.ActiveCfg = Debug|x86 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|x86.Build.0 = Debug|x86 + {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|ARM64.ActiveCfg = Release|ARM64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|ARM64.Build.0 = Release|ARM64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|x64.ActiveCfg = Release|x64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|x64.Build.0 = Release|x64 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|x86.ActiveCfg = Release|x86 {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|x86.Build.0 = Release|x86 + {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BD477B7-FFAB-4864-81F2-18B00130C8E5}.Release|Any CPU.Build.0 = Release|Any CPU {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|ARM64.Build.0 = Debug|ARM64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|x64.ActiveCfg = Debug|x64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|x64.Build.0 = Debug|x64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|x86.ActiveCfg = Debug|x86 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|x86.Build.0 = Debug|x86 + {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Debug|Any CPU.Build.0 = Debug|Any CPU {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|ARM64.ActiveCfg = Release|ARM64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|ARM64.Build.0 = Release|ARM64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|x64.ActiveCfg = Release|x64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|x64.Build.0 = Release|x64 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|x86.ActiveCfg = Release|x86 {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|x86.Build.0 = Release|x86 + {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51D9D3FA-2722-4203-AE21-AEA1B11C761D}.Release|Any CPU.Build.0 = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|ARM64.Build.0 = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|x64.Build.0 = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|x86.Build.0 = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|ARM64.ActiveCfg = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|ARM64.Build.0 = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|x64.ActiveCfg = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|x64.Build.0 = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|x86.ActiveCfg = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|x86.Build.0 = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFFADAEC-2E5A-4F14-B649-2E006558ACA2}.Release|Any CPU.Build.0 = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|ARM64.Build.0 = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|x64.ActiveCfg = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|x64.Build.0 = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|x86.ActiveCfg = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|x86.Build.0 = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|ARM64.ActiveCfg = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|ARM64.Build.0 = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|x64.ActiveCfg = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|x64.Build.0 = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|x86.ActiveCfg = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|x86.Build.0 = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F84E41ED-2A83-4717-99F8-734C7A34A690}.Release|Any CPU.Build.0 = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|ARM64.Build.0 = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|x64.Build.0 = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|x86.Build.0 = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|ARM64.ActiveCfg = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|ARM64.Build.0 = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|x64.ActiveCfg = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|x64.Build.0 = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|x86.ActiveCfg = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|x86.Build.0 = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index c50823fb..11b874bc 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -91,6 +91,11 @@ + + + + + From c48db75b44304f5b36b457d1e1af43e44ffb443e Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 21:48:06 -0500 Subject: [PATCH 02/54] Move StringMethods.cs into Text-Grab.Core Phase 1 of the v5 workflow engine roadmap: StringMethods.cs was already a pure extension-method class with only System.* dependencies, making it a zero-risk first move. It needed CurrentCase and SpotInLine, so those two enums are split out of Text-Grab/Enums.cs into Text-Grab.Core/Enums.cs (same Text_Grab namespace, no using changes needed anywhere); the rest of Enums.cs stays in the app since those enums are UI/settings concerns. Tests/StringMethodTests.cs is left in place unchanged - it still compiles and passes via the transitive Tests -> Text-Grab -> Text-Grab.Core reference chain. It wasn't moved to Tests.Core because one test (TestReverseString) exercises StringBuilderExtensions.ReverseWordsForRightToLeft, which lives in a separate app-side file not covered by this move. --- Text-Grab.Core/Enums.cs | 15 +++++++++++++++ .../Utilities/StringMethods.cs | 0 Text-Grab/Enums.cs | 14 -------------- 3 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 Text-Grab.Core/Enums.cs rename {Text-Grab => Text-Grab.Core}/Utilities/StringMethods.cs (100%) diff --git a/Text-Grab.Core/Enums.cs b/Text-Grab.Core/Enums.cs new file mode 100644 index 00000000..15cb5b30 --- /dev/null +++ b/Text-Grab.Core/Enums.cs @@ -0,0 +1,15 @@ +namespace Text_Grab; + +public enum CurrentCase +{ + Lower = 0, + Camel = 1, + Upper = 2, + Unknown = 3 +} + +public enum SpotInLine +{ + Beginning = 0, + End = 1, +} diff --git a/Text-Grab/Utilities/StringMethods.cs b/Text-Grab.Core/Utilities/StringMethods.cs similarity index 100% rename from Text-Grab/Utilities/StringMethods.cs rename to Text-Grab.Core/Utilities/StringMethods.cs diff --git a/Text-Grab/Enums.cs b/Text-Grab/Enums.cs index d88a3648..264ff58e 100644 --- a/Text-Grab/Enums.cs +++ b/Text-Grab/Enums.cs @@ -19,14 +19,6 @@ public enum TrayIconStyle Monochrome = 1, } -public enum CurrentCase -{ - Lower = 0, - Camel = 1, - Upper = 2, - Unknown = 3 -} - public enum FileStorageKind { Absolute = 0, @@ -65,12 +57,6 @@ public enum Side Bottom = 4 } -public enum SpotInLine -{ - Beginning = 0, - End = 1, -} - public enum TextGrabMode { Fullscreen = 0, From 0fb12657c480cf6713f7e7e33ccd985c25b2df75 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 22:06:25 -0500 Subject: [PATCH 03/54] Move CharacterUtilities.cs and TextSearchUtilities.cs into Text-Grab.Core Continuing Phase 1 of the v5 workflow engine roadmap. Both were already pure (BCL-only usings), making them straightforward moves alongside StringMethods. TextSearchUtilities was internal, visible to the app only through the same-assembly rule and to Tests via Text-Grab's InternalsVisibleTo. Once it moved to Core it's a different assembly from its main consumers (FindAndReplaceWindow, GrabFrame, EditTextWindow), so internal no longer fit; made it public instead, matching StringMethods/CharacterUtilities. --- .../Utilities/CharacterUtilities.cs | 0 .../Utilities/TextSearchUtilities.cs | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) rename {Text-Grab => Text-Grab.Core}/Utilities/CharacterUtilities.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/TextSearchUtilities.cs (76%) diff --git a/Text-Grab/Utilities/CharacterUtilities.cs b/Text-Grab.Core/Utilities/CharacterUtilities.cs similarity index 100% rename from Text-Grab/Utilities/CharacterUtilities.cs rename to Text-Grab.Core/Utilities/CharacterUtilities.cs diff --git a/Text-Grab/Utilities/TextSearchUtilities.cs b/Text-Grab.Core/Utilities/TextSearchUtilities.cs similarity index 76% rename from Text-Grab/Utilities/TextSearchUtilities.cs rename to Text-Grab.Core/Utilities/TextSearchUtilities.cs index 6cb6e2d7..2ca02477 100644 --- a/Text-Grab/Utilities/TextSearchUtilities.cs +++ b/Text-Grab.Core/Utilities/TextSearchUtilities.cs @@ -5,13 +5,13 @@ namespace Text_Grab.Utilities; -internal static class TextSearchUtilities +public static class TextSearchUtilities { private static readonly TimeSpan DefaultRegexTimeout = TimeSpan.FromSeconds(5); - internal static bool HasSearchText(string? searchText) => !string.IsNullOrEmpty(searchText); + public static bool HasSearchText(string? searchText) => !string.IsNullOrEmpty(searchText); - internal static string FormatMatchTextForDisplay(string matchText) + public static string FormatMatchTextForDisplay(string matchText) { if (!matchText.All(char.IsWhiteSpace)) return matchText.MakeStringSingleLine(); @@ -40,7 +40,7 @@ internal static string FormatMatchTextForDisplay(string matchText) return displayText.ToString(); } - internal static Regex CreateFindAndReplaceSearchRegex(string pattern, bool usePatternMode, bool exactMatch) + public static Regex CreateFindAndReplaceSearchRegex(string pattern, bool usePatternMode, bool exactMatch) { RegexOptions options = RegexOptions.Multiline; @@ -50,13 +50,13 @@ internal static Regex CreateFindAndReplaceSearchRegex(string pattern, bool usePa return new Regex(pattern, options, DefaultRegexTimeout); } - internal static Regex CreateReplacementRegex(string pattern, bool exactMatch) + public static Regex CreateReplacementRegex(string pattern, bool exactMatch) { RegexOptions options = exactMatch ? RegexOptions.None : RegexOptions.IgnoreCase; return new Regex(pattern, options, DefaultRegexTimeout); } - internal static Regex CreateGrabFrameSearchRegex(string pattern, bool exactMatch) + public static Regex CreateGrabFrameSearchRegex(string pattern, bool exactMatch) { RegexOptions options = exactMatch ? RegexOptions.Multiline : RegexOptions.Multiline | RegexOptions.IgnoreCase; return new Regex(pattern, options, DefaultRegexTimeout); From 700cab2bcaa060d646bd6efe29ce32446c938b74 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 22:17:04 -0500 Subject: [PATCH 04/54] Move RecognizerExecutor.cs and BuiltInRecognizer.cs into Text-Grab.Core Continuing Phase 1. Scoped to RecognizerExecutor only this round - PatternExecutor stays in the app since PatternItem.GetAll()/GetByName() are wired to the settings singleton, which would cascade into a wider refactor (splitting PatternItem, updating ~6 UI call sites). Both RecognizerExecutor and PatternExecutor called into GrabTemplateExecutor.ExtractMatchesByMode, an internal pure method buried in an otherwise WPF/OCR-coupled 660-line class. Extracted just that method (+ its ExtractByIndices helper) into a new Text-Grab.Core MatchModeSelector so RecognizerExecutor doesn't need to reach back into the app; GrabTemplateExecutor and PatternExecutor now call the Core version too, removing the duplicate logic instead of leaving it behind. Added Microsoft.Recognizers.Text.* package references (+ the existing NuGet.CommandLine exclusion workaround) to Text-Grab.Core, and dropped the now-redundant direct references from Text-Grab.csproj since they flow in transitively through the project reference. --- .../Models/BuiltInRecognizer.cs | 0 Text-Grab.Core/Text-Grab.Core.csproj | 13 +++++++ Text-Grab.Core/Utilities/MatchModeSelector.cs | 39 +++++++++++++++++++ .../Utilities/RecognizerExecutor.cs | 2 +- Text-Grab/Text-Grab.csproj | 5 --- Text-Grab/Utilities/GrabTemplateExecutor.cs | 36 +---------------- Text-Grab/Utilities/PatternExecutor.cs | 2 +- 7 files changed, 55 insertions(+), 42 deletions(-) rename {Text-Grab => Text-Grab.Core}/Models/BuiltInRecognizer.cs (100%) create mode 100644 Text-Grab.Core/Utilities/MatchModeSelector.cs rename {Text-Grab => Text-Grab.Core}/Utilities/RecognizerExecutor.cs (98%) diff --git a/Text-Grab/Models/BuiltInRecognizer.cs b/Text-Grab.Core/Models/BuiltInRecognizer.cs similarity index 100% rename from Text-Grab/Models/BuiltInRecognizer.cs rename to Text-Grab.Core/Models/BuiltInRecognizer.cs diff --git a/Text-Grab.Core/Text-Grab.Core.csproj b/Text-Grab.Core/Text-Grab.Core.csproj index 33c5ae14..914dfad6 100644 --- a/Text-Grab.Core/Text-Grab.Core.csproj +++ b/Text-Grab.Core/Text-Grab.Core.csproj @@ -12,4 +12,17 @@ win-x86;win-x64;win-arm64 + + + + + + + + + none + + + diff --git a/Text-Grab.Core/Utilities/MatchModeSelector.cs b/Text-Grab.Core/Utilities/MatchModeSelector.cs new file mode 100644 index 00000000..e0cc8a99 --- /dev/null +++ b/Text-Grab.Core/Utilities/MatchModeSelector.cs @@ -0,0 +1,39 @@ +namespace Text_Grab.Utilities; + +/// +/// Selects values from an ordered match list according to a mode string +/// ("first", "last", "all", or 1-based indices like "2" / "1,3,5"). Shared by +/// , , and +/// 's placeholder resolution. +/// +public static class MatchModeSelector +{ + public static string ExtractMatchesByMode(IReadOnlyList allValues, string mode, string separator) + { + if (allValues.Count == 0) + return string.Empty; + + return mode.ToLowerInvariant() switch + { + "first" => allValues[0], + "last" => allValues[^1], + "all" => string.Join(separator, allValues), + _ => ExtractByIndices(allValues, mode, separator) + }; + } + + private static string ExtractByIndices(IReadOnlyList values, string mode, string separator) + { + // mode is either a single index like "2" or comma-separated like "1,3,5" + string[] parts = mode.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + List selected = []; + + foreach (string part in parts) + { + if (int.TryParse(part, out int index) && index >= 1 && index <= values.Count) + selected.Add(values[index - 1]); // convert 1-based to 0-based + } + + return string.Join(separator, selected); + } +} diff --git a/Text-Grab/Utilities/RecognizerExecutor.cs b/Text-Grab.Core/Utilities/RecognizerExecutor.cs similarity index 98% rename from Text-Grab/Utilities/RecognizerExecutor.cs rename to Text-Grab.Core/Utilities/RecognizerExecutor.cs index df1de2f8..dfee10c3 100644 --- a/Text-Grab/Utilities/RecognizerExecutor.cs +++ b/Text-Grab.Core/Utilities/RecognizerExecutor.cs @@ -103,7 +103,7 @@ public static string ApplyRecognizer( List values = [.. matches.Select(m => output == RecognizerOutputKind.MatchedText ? m.Text : m.ResolvedValue)]; - return GrabTemplateExecutor.ExtractMatchesByMode(values, matchMode, separator); + return MatchModeSelector.ExtractMatchesByMode(values, matchMode, separator); } // ── Resolution formatting ─────────────────────────────────────────────────── diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index 11b874bc..1429d9e9 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -104,11 +104,6 @@ - - - - - diff --git a/Text-Grab/Utilities/GrabTemplateExecutor.cs b/Text-Grab/Utilities/GrabTemplateExecutor.cs index 03eece92..d5ef78da 100644 --- a/Text-Grab/Utilities/GrabTemplateExecutor.cs +++ b/Text-Grab/Utilities/GrabTemplateExecutor.cs @@ -358,41 +358,7 @@ public static string ApplyPatternPlaceholders( /// Extracts match values based on the mode string. /// internal static string ExtractMatchesByMode(MatchCollection matches, string mode, string separator) - => ExtractMatchesByMode([.. matches.Select(m => m.Value)], mode, separator); - - /// - /// Selects values from an ordered list according to the mode string - /// ("first", "last", "all", or 1-based indices like "2" / "1,3,5"). - /// Shared with recognizer placeholder/application logic. - /// - internal static string ExtractMatchesByMode(IReadOnlyList allValues, string mode, string separator) - { - if (allValues.Count == 0) - return string.Empty; - - return mode.ToLowerInvariant() switch - { - "first" => allValues[0], - "last" => allValues[^1], - "all" => string.Join(separator, allValues), - _ => ExtractByIndices(allValues, mode, separator) - }; - } - - private static string ExtractByIndices(IReadOnlyList values, string mode, string separator) - { - // mode is either a single index like "2" or comma-separated like "1,3,5" - string[] parts = mode.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - List selected = []; - - foreach (string part in parts) - { - if (int.TryParse(part, out int index) && index >= 1 && index <= values.Count) - selected.Add(values[index - 1]); // convert 1-based to 0-based - } - - return string.Join(separator, selected); - } + => MatchModeSelector.ExtractMatchesByMode([.. matches.Select(m => m.Value)], mode, separator); /// /// Resolves entries to their actual regex strings diff --git a/Text-Grab/Utilities/PatternExecutor.cs b/Text-Grab/Utilities/PatternExecutor.cs index 483ad407..527f11ed 100644 --- a/Text-Grab/Utilities/PatternExecutor.cs +++ b/Text-Grab/Utilities/PatternExecutor.cs @@ -68,7 +68,7 @@ public static string Apply( return string.Empty; List values = [.. matches.Select(m => m.Text)]; - return GrabTemplateExecutor.ExtractMatchesByMode(values, mode, separator); + return MatchModeSelector.ExtractMatchesByMode(values, mode, separator); } private static IReadOnlyList GetRegexMatches(string pattern, string text) From e677b543f301b63f3fe501abf726967efbf79476 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 22:20:58 -0500 Subject: [PATCH 05/54] Move StoredRegex.cs and split PatternItem.cs into Text-Grab.Core Continuing Phase 1. StoredRegex.cs was already fully pure, so it moved as-is. PatternItem.cs needed a real split: the class/PatternKind enum are pure, but its GetAll()/GetByName() static methods are wired to AppUtilities.TextGrabSettingsService (the settings singleton). - Text-Grab.Core keeps the PatternItem class (properties + internal constructors + PatternKind enum) - no settings dependency. - New Text-Grab/Models/PatternItemCatalog.cs (staying app-side) carries GetAll()/GetByName(), unchanged apart from the new name. - Added InternalsVisibleTo("Text-Grab") and InternalsVisibleTo("Tests") to Text-Grab.Core so PatternItemCatalog and RegexManager.xaml.cs (which also constructs PatternItem directly) can still reach the internal constructors, and so Tests/PatternExecutorTests.cs (which constructs PatternItem directly for deterministic fixtures) still compiles. - Updated every PatternItem.GetAll()/GetByName() call site to PatternItemCatalog: SearchBar, TextOnlyTemplateDialog, SplitColumnWindow, EditTextWindow, GrabFrame, and the test file. PatternExecutor.cs stays in the app for now - this split is what unblocks moving it next. --- Tests/PatternExecutorTests.cs | 6 +-- Text-Grab.Core/AssemblyInfo.cs | 4 ++ .../Models/PatternItem.cs | 39 --------------- .../Models/StoredRegex.cs | 0 Text-Grab/Controls/SearchBar.xaml.cs | 2 +- Text-Grab/Controls/SplitColumnWindow.xaml.cs | 2 +- .../Controls/TextOnlyTemplateDialog.xaml.cs | 2 +- Text-Grab/Models/PatternItemCatalog.cs | 48 +++++++++++++++++++ Text-Grab/Views/EditTextWindow.xaml.cs | 2 +- Text-Grab/Views/GrabFrame.xaml.cs | 2 +- 10 files changed, 60 insertions(+), 47 deletions(-) create mode 100644 Text-Grab.Core/AssemblyInfo.cs rename {Text-Grab => Text-Grab.Core}/Models/PatternItem.cs (68%) rename {Text-Grab => Text-Grab.Core}/Models/StoredRegex.cs (100%) create mode 100644 Text-Grab/Models/PatternItemCatalog.cs diff --git a/Tests/PatternExecutorTests.cs b/Tests/PatternExecutorTests.cs index 23152e0a..36a4e20e 100644 --- a/Tests/PatternExecutorTests.cs +++ b/Tests/PatternExecutorTests.cs @@ -17,7 +17,7 @@ private static PatternItem RecognizerByName(string name) => [Fact] public void GetAll_ListsSavedRegexesBeforeRecognizers() { - IReadOnlyList all = PatternItem.GetAll(); + IReadOnlyList all = PatternItemCatalog.GetAll(); int firstRecognizer = -1; int lastSaved = -1; @@ -36,7 +36,7 @@ public void GetAll_ListsSavedRegexesBeforeRecognizers() [Fact] public void GetAll_IncludesEveryRecognizerWithSmartGroup() { - List recognizers = [.. PatternItem.GetAll().Where(p => p.Kind == PatternKind.Recognizer)]; + List recognizers = [.. PatternItemCatalog.GetAll().Where(p => p.Kind == PatternKind.Recognizer)]; Assert.Equal(BuiltInRecognizer.GetAll().Count, recognizers.Count); Assert.All(recognizers, p => Assert.Equal(PatternItem.SmartGroup, p.GroupLabel)); @@ -45,7 +45,7 @@ public void GetAll_IncludesEveryRecognizerWithSmartGroup() [Fact] public void GetByName_FindsRecognizer_CaseInsensitive() { - PatternItem? email = PatternItem.GetByName("EMAIL"); + PatternItem? email = PatternItemCatalog.GetByName("EMAIL"); Assert.NotNull(email); Assert.Equal(PatternKind.Recognizer, email!.Kind); diff --git a/Text-Grab.Core/AssemblyInfo.cs b/Text-Grab.Core/AssemblyInfo.cs new file mode 100644 index 00000000..6695779d --- /dev/null +++ b/Text-Grab.Core/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Text-Grab")] +[assembly: InternalsVisibleTo("Tests")] diff --git a/Text-Grab/Models/PatternItem.cs b/Text-Grab.Core/Models/PatternItem.cs similarity index 68% rename from Text-Grab/Models/PatternItem.cs rename to Text-Grab.Core/Models/PatternItem.cs index 0b395929..1099601f 100644 --- a/Text-Grab/Models/PatternItem.cs +++ b/Text-Grab.Core/Models/PatternItem.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Text_Grab.Utilities; - namespace Text_Grab.Models; /// Whether a is backed by a user regex or a built-in recognizer. @@ -83,38 +78,4 @@ internal PatternItem(BuiltInRecognizer recognizer, bool isHidden = false) Recognizer = recognizer; IsHidden = isHidden; } - - /// - /// Returns the combined catalog: the user's saved regexes first (falling back to the - /// built-in defaults when none are saved), then the built-in recognizers. Recognizers the - /// user has hidden are excluded unless is true — the - /// Patterns Manager passes true so it can offer an "unhide" action. - /// - public static IReadOnlyList GetAll(bool includeHidden = false) - { - StoredRegex[] saved = AppUtilities.TextGrabSettingsService.LoadStoredRegexes(); - if (saved.Length == 0) - saved = StoredRegex.GetDefaultPatterns(); - - HashSet hiddenIds = [.. AppUtilities.TextGrabSettingsService.LoadHiddenSmartPatternIds()]; - - IEnumerable recognizers = BuiltInRecognizer.GetAll() - .Select(r => new PatternItem(r, isHidden: hiddenIds.Contains(r.Id))); - - if (!includeHidden) - recognizers = recognizers.Where(r => !r.IsHidden); - - return - [ - .. saved.Select(s => new PatternItem(s)), - .. recognizers, - ]; - } - - /// - /// Finds a pattern by display name (case-insensitive), preferring a saved regex over a - /// recognizer when both share a name. Null when no pattern matches. - /// - public static PatternItem? GetByName(string name) - => GetAll().FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } diff --git a/Text-Grab/Models/StoredRegex.cs b/Text-Grab.Core/Models/StoredRegex.cs similarity index 100% rename from Text-Grab/Models/StoredRegex.cs rename to Text-Grab.Core/Models/StoredRegex.cs diff --git a/Text-Grab/Controls/SearchBar.xaml.cs b/Text-Grab/Controls/SearchBar.xaml.cs index 020d4b9a..54980fa4 100644 --- a/Text-Grab/Controls/SearchBar.xaml.cs +++ b/Text-Grab/Controls/SearchBar.xaml.cs @@ -221,7 +221,7 @@ private void PatternMenu_Opened(object sender, RoutedEventArgs e) PatternMenu.Items.Clear(); string? currentGroup = null; - foreach (PatternItem pattern in PatternItem.GetAll()) + foreach (PatternItem pattern in PatternItemCatalog.GetAll()) { if (pattern.GroupLabel != currentGroup) { diff --git a/Text-Grab/Controls/SplitColumnWindow.xaml.cs b/Text-Grab/Controls/SplitColumnWindow.xaml.cs index c02a0439..b38ebc4e 100644 --- a/Text-Grab/Controls/SplitColumnWindow.xaml.cs +++ b/Text-Grab/Controls/SplitColumnWindow.xaml.cs @@ -58,7 +58,7 @@ private void LoadPatternPicker() { // Feed the inline picker the same unified catalog the Grab Template editor uses: // saved regexes (inserted as {p:Name}) and built-in smart patterns ({r:Name}). - allPatternItems = PatternItem.GetAll(); + allPatternItems = PatternItemCatalog.GetAll(); PatternPickerBox.ItemsSource = [ .. allPatternItems.Select(p => new InlinePickerItem(p.Name, TokenFor(p), p.GroupLabel) diff --git a/Text-Grab/Controls/TextOnlyTemplateDialog.xaml.cs b/Text-Grab/Controls/TextOnlyTemplateDialog.xaml.cs index a9accfce..609f1a99 100644 --- a/Text-Grab/Controls/TextOnlyTemplateDialog.xaml.cs +++ b/Text-Grab/Controls/TextOnlyTemplateDialog.xaml.cs @@ -46,7 +46,7 @@ private void LoadPatternItems() { OutputTemplateBox.ItemsSource = [ - .. PatternItem.GetAll().Select(InlinePickerItemFor), + .. PatternItemCatalog.GetAll().Select(InlinePickerItemFor), ]; } diff --git a/Text-Grab/Models/PatternItemCatalog.cs b/Text-Grab/Models/PatternItemCatalog.cs new file mode 100644 index 00000000..c92b70cc --- /dev/null +++ b/Text-Grab/Models/PatternItemCatalog.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Text_Grab.Utilities; + +namespace Text_Grab.Models; + +/// +/// Loads the combined, user-facing "Patterns" catalog (saved regexes + built-in recognizers) +/// from settings. Split out from because it depends on +/// , which only exists in the app. +/// +public static class PatternItemCatalog +{ + /// + /// Returns the combined catalog: the user's saved regexes first (falling back to the + /// built-in defaults when none are saved), then the built-in recognizers. Recognizers the + /// user has hidden are excluded unless is true — the + /// Patterns Manager passes true so it can offer an "unhide" action. + /// + public static IReadOnlyList GetAll(bool includeHidden = false) + { + StoredRegex[] saved = AppUtilities.TextGrabSettingsService.LoadStoredRegexes(); + if (saved.Length == 0) + saved = StoredRegex.GetDefaultPatterns(); + + HashSet hiddenIds = [.. AppUtilities.TextGrabSettingsService.LoadHiddenSmartPatternIds()]; + + IEnumerable recognizers = BuiltInRecognizer.GetAll() + .Select(r => new PatternItem(r, isHidden: hiddenIds.Contains(r.Id))); + + if (!includeHidden) + recognizers = recognizers.Where(r => !r.IsHidden); + + return + [ + .. saved.Select(s => new PatternItem(s)), + .. recognizers, + ]; + } + + /// + /// Finds a pattern by display name (case-insensitive), preferring a saved regex over a + /// recognizer when both share a name. Null when no pattern matches. + /// + public static PatternItem? GetByName(string name) + => GetAll().FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); +} diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index cf4ffefe..88db5ffc 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -3493,7 +3493,7 @@ private void EditMenuItem_SubmenuOpened(object sender, RoutedEventArgs e) PopulateTemplateMenu(ApplyGrabTemplateMenuItem, textOnlyTemplates, ApplyGrabTemplateItem_Click); PopulateTemplateMenu(ApplyGrabTemplatePerLineMenuItem, textOnlyTemplates, ApplyGrabTemplatePerLineItem_Click); - List patterns = [.. PatternItem.GetAll()]; + List patterns = [.. PatternItemCatalog.GetAll()]; PopulatePatternMenu( ApplyPatternMenuItem, patterns.Where(pattern => pattern.Kind == PatternKind.SavedRegex), diff --git a/Text-Grab/Views/GrabFrame.xaml.cs b/Text-Grab/Views/GrabFrame.xaml.cs index 104e623f..73e378d0 100644 --- a/Text-Grab/Views/GrabFrame.xaml.cs +++ b/Text-Grab/Views/GrabFrame.xaml.cs @@ -4775,7 +4775,7 @@ private void UpdateTemplatePickerItems() // Pattern items — saved regexes and built-in recognizers as one "Patterns" concept, // split into "Saved Patterns" / "Smart Patterns" subsections. - items.AddRange(PatternItem.GetAll().Select(TextOnlyTemplateDialog.InlinePickerItemFor)); + items.AddRange(PatternItemCatalog.GetAll().Select(TextOnlyTemplateDialog.InlinePickerItemFor)); TemplateOutputBox.ItemsSource = items; From c110cd1380929e053c4920d7ac3b4791b15e93cf Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 22:25:57 -0500 Subject: [PATCH 06/54] Move ILanguage.cs and OcrLinesWords.cs into Text-Grab.Core.Windows First move into Core.Windows, starting Phase 4 of the v5 workflow engine roadmap. Both are pure interface contracts (ILanguage needs Windows.Globalization, IOcrLinesWords/IOcrLine/IOcrWord need Windows.Foundation.Rect) with zero implementation logic and zero WPF coupling - the Windows SDK projections resolve automatically from the net10.0-windows10.0.22621.0 TFM, no extra package needed. No consumer call sites needed touching: implementers (TessLang, GlobalLang, WinRtOcrLinesWords, etc.) stay in the app and keep resolving the interfaces transitively through the existing project reference. --- {Text-Grab => Text-Grab.Core.Windows}/Interfaces/ILanguage.cs | 0 {Text-Grab => Text-Grab.Core.Windows}/Models/OcrLinesWords.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Interfaces/ILanguage.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/OcrLinesWords.cs (100%) diff --git a/Text-Grab/Interfaces/ILanguage.cs b/Text-Grab.Core.Windows/Interfaces/ILanguage.cs similarity index 100% rename from Text-Grab/Interfaces/ILanguage.cs rename to Text-Grab.Core.Windows/Interfaces/ILanguage.cs diff --git a/Text-Grab/Models/OcrLinesWords.cs b/Text-Grab.Core.Windows/Models/OcrLinesWords.cs similarity index 100% rename from Text-Grab/Models/OcrLinesWords.cs rename to Text-Grab.Core.Windows/Models/OcrLinesWords.cs From edefeaa6bdb0c311731c11fe95b5c03a41b40c7a Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 22:36:59 -0500 Subject: [PATCH 07/54] Move the OCR engine implementations into Text-Grab.Core.Windows Continuing Phase 4. Moved the clean tier identified after mapping the whole OCR/HDR area: TessLang, GlobalLang, WinAiOcrLinesWords (all already dependency-free beyond ILanguage/IOcrLinesWords, which moved earlier), and WinRtOcrLinesWords. WinRtOcrLinesWords needed one fix: its constructor called OcrUtilities.GetBoundingRect() - an app-side extension method that computes the union of a line's word rects but returns a WPF System.Windows.Rect, which the constructor then converted back into a WinRT Windows.Foundation.Rect. Since every input (OcrWord.BoundingRect) is already a WinRT Rect, that was a pointless WPF round-trip and the actual blocker to moving the file. Replaced it with a local private helper that computes the same union directly in Windows.Foundation.Rect. OcrUtilities.GetBoundingRect itself is untouched - other app code may still use it. Also extracted HocrReader + TessOcrLine (the last ~53 lines of TesseractHelper.cs) into Text-Grab.Core - pure string/regex hOCR parsing, already cleanly separated from the rest of the file, going to plain Core (not .Windows) since it's not Windows-specific. The rest of TesseractHelper.cs stays in the app; nothing else currently calls it. Added Microsoft.WindowsAppSDK.AI to Text-Grab.Core.Windows.csproj for WinAiOcrLinesWords' RecognizedText/RecognizedLine/RecognizedWord types. Deferred (each documented in the plan with its specific blocker): OcrUtilities.cs (the actual "pivotal cut" - 1061 lines, mixed headless/UI-adapter code plus a hidden WPF dependency in LoadBitmapFromFile), the TesseractHelper class itself (settings-write side effect in GetTesseractPath), LanguageService.cs and WindowsAiUtilities.cs (both settings+WPF coupled, each dragging in several more untouched types), BarcodeUtilities.cs (blocked on OcrOutput.CleanOutput() reading settings directly), and the HDR/WGC capture code (separate area, mostly clean already). --- .../Models/GlobalLang.cs | 0 .../Models/TessLang.cs | 0 .../Models/WinAiOcrLinesWords.cs | 0 .../Models/WinRtOcrLinesWords.cs | 17 ++++-- .../Text-Grab.Core.Windows.csproj | 5 ++ Text-Grab.Core/Utilities/HocrReader.cs | 57 +++++++++++++++++++ Text-Grab/Utilities/TesseractHelper.cs | 54 ------------------ 7 files changed, 74 insertions(+), 59 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Models/GlobalLang.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/TessLang.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/WinAiOcrLinesWords.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/WinRtOcrLinesWords.cs (74%) create mode 100644 Text-Grab.Core/Utilities/HocrReader.cs diff --git a/Text-Grab/Models/GlobalLang.cs b/Text-Grab.Core.Windows/Models/GlobalLang.cs similarity index 100% rename from Text-Grab/Models/GlobalLang.cs rename to Text-Grab.Core.Windows/Models/GlobalLang.cs diff --git a/Text-Grab/Models/TessLang.cs b/Text-Grab.Core.Windows/Models/TessLang.cs similarity index 100% rename from Text-Grab/Models/TessLang.cs rename to Text-Grab.Core.Windows/Models/TessLang.cs diff --git a/Text-Grab/Models/WinAiOcrLinesWords.cs b/Text-Grab.Core.Windows/Models/WinAiOcrLinesWords.cs similarity index 100% rename from Text-Grab/Models/WinAiOcrLinesWords.cs rename to Text-Grab.Core.Windows/Models/WinAiOcrLinesWords.cs diff --git a/Text-Grab/Models/WinRtOcrLinesWords.cs b/Text-Grab.Core.Windows/Models/WinRtOcrLinesWords.cs similarity index 74% rename from Text-Grab/Models/WinRtOcrLinesWords.cs rename to Text-Grab.Core.Windows/Models/WinRtOcrLinesWords.cs index b4351d4b..39aa1cdb 100644 --- a/Text-Grab/Models/WinRtOcrLinesWords.cs +++ b/Text-Grab.Core.Windows/Models/WinRtOcrLinesWords.cs @@ -1,5 +1,4 @@ -using Text_Grab.Utilities; -using Windows.Foundation; +using Windows.Foundation; using Windows.Media.Ocr; namespace Text_Grab.Models; @@ -41,9 +40,7 @@ public WinRtOcrLine(OcrLine ocrLine) Words[i] = new WinRtOcrWord(word); } - System.Windows.Rect bRect = ocrLine.GetBoundingRect(); - - BoundingBox = new Rect(bRect.Left, bRect.Top, bRect.Width, bRect.Height); + BoundingBox = GetBoundingRect(ocrLine); } public OcrLine OriginalLine { get; set; } @@ -51,6 +48,16 @@ public WinRtOcrLine(OcrLine ocrLine) public string Text { get; set; } public IOcrWord[] Words { get; set; } public Rect BoundingBox { get; set; } + + private static Rect GetBoundingRect(OcrLine ocrLine) + { + double top = ocrLine.Words.Select(w => w.BoundingRect.Top).Min(); + double bottom = ocrLine.Words.Select(w => w.BoundingRect.Bottom).Max(); + double left = ocrLine.Words.Select(w => w.BoundingRect.Left).Min(); + double right = ocrLine.Words.Select(w => w.BoundingRect.Right).Max(); + + return new Rect(left, top, Math.Abs(right - left), Math.Abs(bottom - top)); + } } public class WinRtOcrWord : IOcrWord diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index acc77374..5e48e510 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -16,4 +16,9 @@ + + + + + diff --git a/Text-Grab.Core/Utilities/HocrReader.cs b/Text-Grab.Core/Utilities/HocrReader.cs new file mode 100644 index 00000000..7d9b0251 --- /dev/null +++ b/Text-Grab.Core/Utilities/HocrReader.cs @@ -0,0 +1,57 @@ +using System.Text.RegularExpressions; + +namespace Text_Grab.Utilities; + +public class TessOcrLine +{ + public int Height { get; set; } + public string Text { get; set; } = string.Empty; + public int Width { get; set; } + public int X { get; set; } + public int Y { get; set; } +} + +public static class HocrReader +{ + private static readonly string[] separator = [""]; + + public static List ReadLines(string hocrText) + { + // Create a list to hold the OcrLine objects + List lines = new(); + + // Split the hOCR text into lines + string[] hocrLines = hocrText.Split(separator, StringSplitOptions.RemoveEmptyEntries); + + // Iterate through the lines + foreach (string hocrLineText in hocrLines) + { + // Extract the line information + TessOcrLine line = ReadLine(hocrLineText); + + // Add the line to the list + lines.Add(line); + } + + return lines; + } + + private static TessOcrLine ReadLine(string hocrLineText) + { + // Create a new OcrLine object + TessOcrLine line = new(); + + // Extract the text of the line from the hOCR text + Match textMatch = Regex.Match(hocrLineText, "]*>(.*?)"); + line.Text = textMatch.Groups[1].Value; + + // Extract the bounding box coordinates from the hOCR text + Match bboxMatch = Regex.Match(hocrLineText, "bbox (\\d+) (\\d+) (\\d+) (\\d+)"); + line.X = int.Parse(bboxMatch.Groups[1].Value); + line.Y = int.Parse(bboxMatch.Groups[2].Value); + line.Width = int.Parse(bboxMatch.Groups[3].Value); + line.Height = int.Parse(bboxMatch.Groups[4].Value); + + return line; + } +} diff --git a/Text-Grab/Utilities/TesseractHelper.cs b/Text-Grab/Utilities/TesseractHelper.cs index 9dfdccb6..12482112 100644 --- a/Text-Grab/Utilities/TesseractHelper.cs +++ b/Text-Grab/Utilities/TesseractHelper.cs @@ -399,57 +399,3 @@ public async Task DownloadFileAsync(string filenameToDownload, string localDesti "yor.traineddata", ]; } - -public class TessOcrLine -{ - public int Height { get; set; } - public string Text { get; set; } = string.Empty; - public int Width { get; set; } - public int X { get; set; } - public int Y { get; set; } -} - -public static class HocrReader -{ - private static readonly string[] separator = [""]; - - public static List ReadLines(string hocrText) - { - // Create a list to hold the OcrLine objects - List lines = new(); - - // Split the hOCR text into lines - string[] hocrLines = hocrText.Split(separator, StringSplitOptions.RemoveEmptyEntries); - - // Iterate through the lines - foreach (string hocrLineText in hocrLines) - { - // Extract the line information - TessOcrLine line = ReadLine(hocrLineText); - - // Add the line to the list - lines.Add(line); - } - - return lines; - } - - private static TessOcrLine ReadLine(string hocrLineText) - { - // Create a new OcrLine object - TessOcrLine line = new(); - - // Extract the text of the line from the hOCR text - Match textMatch = Regex.Match(hocrLineText, "]*>(.*?)"); - line.Text = textMatch.Groups[1].Value; - - // Extract the bounding box coordinates from the hOCR text - Match bboxMatch = Regex.Match(hocrLineText, "bbox (\\d+) (\\d+) (\\d+) (\\d+)"); - line.X = int.Parse(bboxMatch.Groups[1].Value); - line.Y = int.Parse(bboxMatch.Groups[2].Value); - line.Width = int.Parse(bboxMatch.Groups[3].Value); - line.Height = int.Parse(bboxMatch.Groups[4].Value); - - return line; - } -} From 30af90f1f76c74fbfa0a6a85b8579455a0c828fd Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 23:43:41 -0500 Subject: [PATCH 08/54] Establish RectangleF as the Core tier's geometry currency Wave 0a of the Core split. No files move; this only puts the substitute type and the boundary conversions in place so later waves have somewhere to land. System.Windows.Rect/Point/Size live in WindowsBase.dll and only exist with UseWPF=true. They appear in roughly thirty otherwise-portable files and are the second-biggest blocker to moving code out of the app, after settings access. Text-Grab.Core targets plain net10.0 and Text-Grab.Core.Windows deliberately keeps UseWPF=false, so neither can use them. RectangleF/PointF/SizeF are the substitute. They come from System.Drawing.Primitives, which is part of the shared framework and genuinely cross-platform - not to be confused with System.Drawing.Common (Bitmap, Graphics, Icon), which is Windows-only and belongs in Text-Grab.Core.Windows. A bare "using System.Drawing" says nothing about which of the two a file needs; check the types. - Text-Grab.Core/Utilities/RectangleFExtensions.cs mirrors the portable half of the app's ShapeExtensions: IsGood, CenterPoint, GetScaledUpByFraction, GetScaleSizeByFraction. Each one already has a Rect-typed caller in the app, so none of these are speculative. Union is the exception - it is new, and skips empty operands instead of dragging the result back to the origin so it can be folded over a sequence of word rects, which is what ResultTable and the OCR line assembly will want in wave 4. - ShapeExtensions gains the conversions across the boundary: AsRect / AsRectangleF, AsPoint / AsPointF, AsSize / AsSizeF. It already carried Rectangle <-> Rect, so this extends the existing seam rather than standing up a parallel WpfGeometryExtensions next to it. Tests.Core covers the new helpers, which also gives that project real tests instead of only the scaffolding smoke test. Co-Authored-By: Claude Opus 5 --- Tests.Core/RectangleFExtensionsTests.cs | 66 ++++++++++++++++ .../Utilities/RectangleFExtensions.cs | 75 +++++++++++++++++++ Text-Grab/Extensions/ShapeExtensions.cs | 34 +++++++++ 3 files changed, 175 insertions(+) create mode 100644 Tests.Core/RectangleFExtensionsTests.cs create mode 100644 Text-Grab.Core/Utilities/RectangleFExtensions.cs diff --git a/Tests.Core/RectangleFExtensionsTests.cs b/Tests.Core/RectangleFExtensionsTests.cs new file mode 100644 index 00000000..f5e154db --- /dev/null +++ b/Tests.Core/RectangleFExtensionsTests.cs @@ -0,0 +1,66 @@ +using System.Drawing; + +namespace Text_Grab.Tests.Core; + +public class RectangleFExtensionsTests +{ + [Theory] + [InlineData(0, 0, 10, 10, true)] + [InlineData(-5, -5, 1, 1, true)] + [InlineData(0, 0, 0, 10, false)] // zero width + [InlineData(0, 0, 10, 0, false)] // zero height + [InlineData(float.NaN, 0, 10, 10, false)] + [InlineData(0, float.NaN, 10, 10, false)] + [InlineData(float.PositiveInfinity, 0, 10, 10, false)] + [InlineData(0, 0, float.NegativeInfinity, 10, false)] + public void IsGood_RejectsDegenerateAndNonFiniteRects(float x, float y, float w, float h, bool expected) + { + RectangleF rect = new(x, y, w, h); + + Assert.Equal(expected, rect.IsGood()); + } + + [Fact] + public void CenterPoint_ReturnsMidpoint() + { + RectangleF rect = new(10, 20, 30, 40); + + PointF center = rect.CenterPoint(); + + Assert.Equal(25f, center.X); + Assert.Equal(40f, center.Y); + } + + [Fact] + public void GetScaledUpByFraction_ScalesPositionAndSize() + { + RectangleF scaled = new RectangleF(10, 20, 30, 40).GetScaledUpByFraction(2.0); + + Assert.Equal(new RectangleF(20, 40, 60, 80), scaled); + } + + [Fact] + public void GetScaleSizeByFraction_LeavesOriginInPlace() + { + RectangleF scaled = new RectangleF(10, 20, 30, 40).GetScaleSizeByFraction(0.5); + + Assert.Equal(new RectangleF(10, 20, 15, 20), scaled); + } + + [Fact] + public void Union_CombinesBothRects() + { + RectangleF union = new RectangleF(0, 0, 10, 10).Union(new RectangleF(20, 20, 10, 10)); + + Assert.Equal(new RectangleF(0, 0, 30, 30), union); + } + + [Fact] + public void Union_IgnoresEmptyOperandsRatherThanPullingToOrigin() + { + RectangleF populated = new(50, 50, 10, 10); + + Assert.Equal(populated, populated.Union(RectangleF.Empty)); + Assert.Equal(populated, RectangleF.Empty.Union(populated)); + } +} diff --git a/Text-Grab.Core/Utilities/RectangleFExtensions.cs b/Text-Grab.Core/Utilities/RectangleFExtensions.cs new file mode 100644 index 00000000..6f504a92 --- /dev/null +++ b/Text-Grab.Core/Utilities/RectangleFExtensions.cs @@ -0,0 +1,75 @@ +using System.Drawing; + +namespace Text_Grab; + +/// +/// Portable geometry helpers for the Core tier. +/// +/// Text-Grab's UI code works in System.Windows.Rect, which lives in WindowsBase.dll and +/// is therefore only available with UseWPF=true. Text-Grab.Core targets plain net10.0 and +/// Text-Grab.Core.Windows deliberately keeps UseWPF=false, so neither can use it. +/// +/// is the substitute: it lives in System.Drawing.Primitives, which is +/// part of the shared framework and genuinely cross-platform - unlike System.Drawing.Common's +/// Bitmap/Graphics, which are Windows-only and belong in Text-Grab.Core.Windows. +/// +/// These mirror the WPF-typed helpers in Text-Grab/Extensions/ShapeExtensions.cs, which also +/// carries the conversions across the boundary (AsRect / AsRectangleF). +/// +public static class RectangleFExtensions +{ + /// + /// Whether the rectangle is usable for layout or hit-testing: finite on every axis and + /// non-degenerate. Mirrors ShapeExtensions.IsGood(Rect). + /// + public static bool IsGood(this RectangleF rect) + { + if (float.IsNaN(rect.X) || float.IsInfinity(rect.X)) + return false; + + if (float.IsNaN(rect.Y) || float.IsInfinity(rect.Y)) + return false; + + if (float.IsNaN(rect.Height) || rect.Height == 0 || float.IsInfinity(rect.Height)) + return false; + + if (float.IsNaN(rect.Width) || rect.Width == 0 || float.IsInfinity(rect.Width)) + return false; + + return true; + } + + public static PointF CenterPoint(this RectangleF rect) + => new(rect.Left + (rect.Width / 2), rect.Top + (rect.Height / 2)); + + /// Scales position and size together, keeping the rectangle in the same relative spot. + public static RectangleF GetScaledUpByFraction(this RectangleF rect, double scaleFactor) + => new( + (float)(rect.X * scaleFactor), + (float)(rect.Y * scaleFactor), + (float)(rect.Width * scaleFactor), + (float)(rect.Height * scaleFactor)); + + /// Scales size only, leaving the top-left corner where it is. + public static RectangleF GetScaleSizeByFraction(this RectangleF rect, double scaleFactor) + => new( + rect.X, + rect.Y, + (float)(rect.Width * scaleFactor), + (float)(rect.Height * scaleFactor)); + + /// + /// The smallest rectangle containing both inputs. An empty input is ignored rather than + /// dragging the union back to the origin, so this can be folded over a sequence. + /// + public static RectangleF Union(this RectangleF rect, RectangleF other) + { + if (rect.IsEmpty) + return other; + + if (other.IsEmpty) + return rect; + + return RectangleF.Union(rect, other); + } +} diff --git a/Text-Grab/Extensions/ShapeExtensions.cs b/Text-Grab/Extensions/ShapeExtensions.cs index d666960b..caab4d66 100644 --- a/Text-Grab/Extensions/ShapeExtensions.cs +++ b/Text-Grab/Extensions/ShapeExtensions.cs @@ -16,6 +16,40 @@ public static Rectangle AsRectangle(this Rect rect) return new Rectangle((int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height); } + // Conversions across the Core boundary. Text-Grab.Core and Text-Grab.Core.Windows cannot use + // System.Windows.Rect (WindowsBase, WPF-only), so they speak RectangleF/PointF/SizeF from + // System.Drawing.Primitives instead. View code converts here, at the edge. + + public static Rect AsRect(this RectangleF rectangle) + { + return new Rect(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + + public static RectangleF AsRectangleF(this Rect rect) + { + return new RectangleF((float)rect.X, (float)rect.Y, (float)rect.Width, (float)rect.Height); + } + + public static System.Windows.Point AsPoint(this PointF point) + { + return new System.Windows.Point(point.X, point.Y); + } + + public static PointF AsPointF(this System.Windows.Point point) + { + return new PointF((float)point.X, (float)point.Y); + } + + public static System.Windows.Size AsSize(this SizeF size) + { + return new System.Windows.Size(size.Width, size.Height); + } + + public static SizeF AsSizeF(this System.Windows.Size size) + { + return new SizeF((float)size.Width, (float)size.Height); + } + public static Rect GetScaledDownByDpi(this Rect rect, DpiScale dpi) { return new Rect(rect.X / dpi.DpiScaleX, From 8398af26f34c51ae48f522a621f5a982f252ad2c Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 23:45:30 -0500 Subject: [PATCH 09/54] Give Core a settings seam via ITextGrabSettings Wave 0b of the Core split. No files move; this unblocks the ones that could not move because they read settings. 48 app files reach settings through AppUtilities.TextGrabSettings, which returns Text_Grab.Properties.Settings - an internal, sealed, generated ApplicationSettingsBase with 104 properties. That accessor is the single largest thing tying logic to the app assembly, and it is why OcrUtilities, TesseractHelper, LanguageService, WindowsAiUtilities and BarcodeUtilities are all sitting in the deferred ledger. The useful detail is that the coupling is much narrower than the file count suggests: only 28 distinct properties are read through that accessor across the entire app. - Text-Grab.Core/Interfaces/ITextGrabSettings.cs declares the slice portable code may read. It is seeded from the actual near-term consumers rather than guessed at: CorrectErrors, CorrectToLatin, ParagraphDetection, RemoveFurigana, TryToReadBarcodes and UiAutomationFallbackToOcr are what OcrUtilities reads via its DefaultSettings field; TesseractPath and Save() are what TesseractHelper.GetTesseractPath needs; CorrectToLatin and CorrectErrors are what OcrOutput.CleanOutput reads, which is the specific thing blocking BarcodeUtilities. - The app implements it by declaring it. Settings.Designer.cs already generates properties with matching names and types, and Save() comes from ApplicationSettingsBase, so the hand-written partial in Properties/Settings.cs satisfies the whole interface with no forwarding code. An internal class implementing a public interface is fine, and this keeps working across SettingsSingleFileGenerator runs. - SettingsAccess holds a resolver delegate rather than an instance. The app's settings object hangs off Singleton.Instance, which is lazy and does real work on first touch - reads user.config, seeds automation profiles, loads JSON sidecars. Storing a delegate keeps module initialization free of all that while leaving the existing lazy behavior exactly as it was. - Registration is a [ModuleInitializer] in the app, not a call in App.appStartup. The Tests host loads the app assembly and exercises its code without ever raising the WPF Startup event; wiring it at module load covers both paths by construction. SettingsAccessTests asserts precisely that - it passes without any startup call. Keep the interface small. Add a property when a move demands it; if one file would need more than a handful, prefer the facade split instead - pure logic to Core, thin settings-reading wrapper left in the app, the way PatternItem / PatternItemCatalog went in e677b54. Co-Authored-By: Claude Opus 5 --- Tests/SettingsAccessTests.cs | 86 +++++++++++++++++++ .../Interfaces/ITextGrabSettings.cs | 52 +++++++++++ Text-Grab.Core/Services/SettingsAccess.cs | 50 +++++++++++ Text-Grab/Properties/Settings.cs | 9 +- .../Utilities/SettingsAccessInitializer.cs | 23 +++++ 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 Tests/SettingsAccessTests.cs create mode 100644 Text-Grab.Core/Interfaces/ITextGrabSettings.cs create mode 100644 Text-Grab.Core/Services/SettingsAccess.cs create mode 100644 Text-Grab/Utilities/SettingsAccessInitializer.cs diff --git a/Tests/SettingsAccessTests.cs b/Tests/SettingsAccessTests.cs new file mode 100644 index 00000000..79a34657 --- /dev/null +++ b/Tests/SettingsAccessTests.cs @@ -0,0 +1,86 @@ +using Text_Grab.Interfaces; +using Text_Grab.Properties; +using Text_Grab.Services; + +namespace Tests; + +/// +/// Guards the seam that lets Text-Grab.Core read settings without referencing the app. +/// If these break, portable code either cannot reach settings or is silently reading the wrong +/// instance - both of which surface far away from the actual cause. +/// +[Collection("Settings isolation")] +public class SettingsAccessTests +{ + [Fact] + public void ModuleInitializer_RegistersAResolverWithoutAnyStartupCall() + { + // The test host never raises App's WPF Startup event, so this passing is the proof that + // wiring lives in a module initializer and not in appStartup. + Assert.True(SettingsAccess.IsConfigured); + } + + [Fact] + public void Current_ResolvesToTheAppSettingsObject() + { + ITextGrabSettings settings = SettingsAccess.Current; + + Assert.IsType(settings); + } + + [Fact] + public void GeneratedSettingsPropertiesSatisfyTheInterfaceWithoutForwarding() + { + // Settings.Designer.cs is regenerated by SettingsSingleFileGenerator. Reading and writing + // through the interface here is what catches a generated property being renamed or having + // its type changed out from under ITextGrabSettings. + ITextGrabSettings settings = new Settings(); + + settings.CorrectToLatin = true; + settings.ParagraphDetection = false; + settings.TesseractPath = @"C:\tesseract\tesseract.exe"; + settings.LastUsedLang = "ja-JP"; + + Assert.True(settings.CorrectToLatin); + Assert.False(settings.ParagraphDetection); + Assert.Equal(@"C:\tesseract\tesseract.exe", settings.TesseractPath); + Assert.Equal("ja-JP", settings.LastUsedLang); + } + + [Fact] + public void SetResolver_SubstitutesAFakeAndCanBeRestored() + { + Settings substitute = new() { CorrectErrors = false, RemoveFurigana = true }; + + try + { + SettingsAccess.SetResolver(() => substitute); + + Assert.Same(substitute, SettingsAccess.Current); + Assert.False(SettingsAccess.Current.CorrectErrors); + Assert.True(SettingsAccess.Current.RemoveFurigana); + } + finally + { + SettingsAccess.SetResolver(static () => Text_Grab.Utilities.AppUtilities.TextGrabSettings); + } + + Assert.NotSame(substitute, SettingsAccess.Current); + } + + [Fact] + public void Current_ThrowsAClearErrorWhenNoResolverIsRegistered() + { + try + { + SettingsAccess.ClearResolver(); + + InvalidOperationException ex = Assert.Throws(() => SettingsAccess.Current); + Assert.Contains(nameof(SettingsAccess.SetResolver), ex.Message); + } + finally + { + SettingsAccess.SetResolver(static () => Text_Grab.Utilities.AppUtilities.TextGrabSettings); + } + } +} diff --git a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs new file mode 100644 index 00000000..6efc4962 --- /dev/null +++ b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs @@ -0,0 +1,52 @@ +namespace Text_Grab.Interfaces; + +/// +/// The slice of Text-Grab's user settings that portable (Core / Core.Windows) code is allowed +/// to read. +/// +/// The app's real settings object is Text_Grab.Properties.Settings - an internal, sealed, +/// generated ApplicationSettingsBase with 104 properties, reachable only through +/// AppUtilities.TextGrabSettings. That accessor is the single largest thing tying logic to +/// the app assembly. This interface breaks that tie without moving the settings machinery: the +/// generated properties already match these names and types, so the hand-written partial in +/// Text-Grab/Properties/Settings.cs satisfies the whole interface just by declaring it. +/// +/// Keep this deliberately small. Add a property only when a file being moved actually reads it. +/// If a single move would need more than a handful of new members, that file probably wants the +/// facade split instead - move the pure logic to Core and leave a thin settings-reading wrapper +/// in the app, the way PatternItem / PatternItemCatalog was handled in e677b54. +/// +/// Resolved through . +/// +public interface ITextGrabSettings +{ + /// Apply the OCR error-correction pass to recognized text. + bool CorrectErrors { get; set; } + + /// Map look-alike Greek and Cyrillic characters to Latin. + bool CorrectToLatin { get; set; } + + /// Join OCR lines into paragraphs instead of preserving line breaks. + bool ParagraphDetection { get; set; } + + /// Strip furigana ruby text from recognized Japanese. + bool RemoveFurigana { get; set; } + + /// Scan captured images for barcodes and QR codes. + bool TryToReadBarcodes { get; set; } + + /// Fall back to OCR when UI Automation returns no text. + bool UiAutomationFallbackToOcr { get; set; } + + /// Route OCR through Tesseract instead of the Windows engines. + bool UseTesseract { get; set; } + + /// Cached path to the Tesseract executable; written back once discovered. + string TesseractPath { get; set; } + + /// BCP-47 tag of the language used for the last capture. + string LastUsedLang { get; set; } + + /// Persist pending changes. Backed by ApplicationSettingsBase.Save(). + void Save(); +} diff --git a/Text-Grab.Core/Services/SettingsAccess.cs b/Text-Grab.Core/Services/SettingsAccess.cs new file mode 100644 index 00000000..99811676 --- /dev/null +++ b/Text-Grab.Core/Services/SettingsAccess.cs @@ -0,0 +1,50 @@ +using System; +using Text_Grab.Interfaces; + +namespace Text_Grab.Services; + +/// +/// How portable code reaches user settings. +/// +/// Core cannot call AppUtilities.TextGrabSettings - that lives in the app assembly and +/// returns an internal type. Instead the app registers a resolver at module load and Core reads +/// through . +/// +/// The resolver is a delegate rather than a stored instance on purpose: the app's settings object +/// hangs off Singleton<SettingsService>.Instance, which is lazy and does real work on +/// first touch (reads user.config, seeds automation profiles, loads JSON sidecars). Registering a +/// delegate keeps module initialization free of that; the settings object is still built on first +/// read, exactly as it is today. +/// +public static class SettingsAccess +{ + private static Func? _resolver; + + /// + /// Registers the source of settings. The app calls this from a module initializer, so it is + /// in place for any entry point into the app assembly - including the test host, which never + /// runs App.appStartup. Tests may call it again to substitute a fake. + /// + public static void SetResolver(Func resolver) + => _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + + /// Drops the registered resolver. For tests that need to restore a clean slate. + public static void ClearResolver() => _resolver = null; + + /// Whether a resolver has been registered. + public static bool IsConfigured => _resolver is not null; + + /// + /// The active settings. + /// + /// + /// No resolver was registered. In the app this cannot happen - the module initializer covers + /// it. It means Core code is running with neither the app assembly loaded nor a test fake + /// installed, so the caller has to supply one. + /// + public static ITextGrabSettings Current + => _resolver?.Invoke() + ?? throw new InvalidOperationException( + $"No settings resolver registered. Call {nameof(SettingsAccess)}.{nameof(SetResolver)} " + + "before reading settings from Core code."); +} diff --git a/Text-Grab/Properties/Settings.cs b/Text-Grab/Properties/Settings.cs index a7de5c03..f6d9a3c8 100644 --- a/Text-Grab/Properties/Settings.cs +++ b/Text-Grab/Properties/Settings.cs @@ -1,4 +1,5 @@ using System.Configuration; +using Text_Grab.Interfaces; using Text_Grab.Utilities; namespace Text_Grab.Properties; @@ -10,7 +11,13 @@ namespace Text_Grab.Properties; // is active the provider transparently defers to its LocalFileSettingsProvider base, // so normal runs are unaffected. This lives in a hand-written partial so it survives // SettingsSingleFileGenerator regenerating Settings.Designer.cs. +// +// This partial also declares ITextGrabSettings, which is how Text-Grab.Core reads settings +// without depending on the app. Every member of that interface is already implemented by the +// generated properties (and by ApplicationSettingsBase.Save), so there is nothing to write here - +// declaring the interface is the whole implementation. If a build breaks on a newly added +// interface member, the fix belongs in Settings.settings, not in a forwarding property here. [SettingsProvider(typeof(AutomationSettingsProvider))] -internal sealed partial class Settings +internal sealed partial class Settings : ITextGrabSettings { } diff --git a/Text-Grab/Utilities/SettingsAccessInitializer.cs b/Text-Grab/Utilities/SettingsAccessInitializer.cs new file mode 100644 index 00000000..18ca92ad --- /dev/null +++ b/Text-Grab/Utilities/SettingsAccessInitializer.cs @@ -0,0 +1,23 @@ +using System.Runtime.CompilerServices; +using Text_Grab.Services; + +namespace Text_Grab.Utilities; + +/// +/// Points Text-Grab.Core's at the app's real settings object. +/// +internal static class SettingsAccessInitializer +{ + /// + /// Runs when the Text-Grab assembly loads, before any of its code executes. A module + /// initializer rather than a call in App.appStartup because the Tests host loads this + /// assembly and exercises its code without ever raising the WPF Startup event - wiring it + /// here means both paths are covered by construction. + /// + /// This only stores the delegate. AppUtilities.TextGrabSettings still resolves lazily on + /// first read, so nothing forces SettingsService to be built at load time. + /// + [ModuleInitializer] + internal static void Initialize() + => SettingsAccess.SetResolver(static () => AppUtilities.TextGrabSettings); +} From d7edfcad26dcd866ae1398eef6aa769f1dc87456 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 26 Aug 2026 23:48:28 -0500 Subject: [PATCH 10/54] Scaffold Tests.Core.Windows, guard the tier boundaries, wire CI Wave 0c of the Core split, and the last of the foundation work before files start moving. - New Tests.Core.Windows project for the headless Windows tier, mirroring Text-Grab.Core.Windows: same TFM, UseWPF and UseWindowsForms both off, so a test can never pass against a reference the library itself cannot use. It needs an explicit Platform - without one the Windows App SDK targets pulled in transitively via Microsoft.WindowsAppSDK.AI fail with "WindowsAppSDKSelfContained requires a supported Windows architecture". Build and test it with -p:Platform=x64. - TierBoundaryTests enforces the layering by reflection rather than by review: Text-Grab.Core references no WPF, WinForms, System.Drawing.Common or Windows App SDK assembly; Text-Grab.Core.Windows references no WPF assembly; and Core does not reference Core.Windows. This was not in the original plan, but the risk register lists "a mover flips UseWPF=true to unblock itself" as a real failure mode, and a csproj diff is easy to wave through in review. Now it fails the build instead. - InternalsVisibleTo brought up to date on both libraries, so the two new test projects can reach internal types as more code moves. - Both workflows now run all three test projects. The pure tiers run first: they need no display and finish in about a second each, so a logic regression fails the job before the slow WPF/STA suite starts. - docs/Core-Split-Plan.md is the standing plan for waves 1 through 7 and the contract every mover agent reads before touching anything. Its section 3 is updated to describe what wave 0 actually built rather than what it proposed, including the three places the result differed: ShapeExtensions was extended instead of adding a parallel WpfGeometryExtensions, settings registration moved to a [ModuleInitializer] so the test host is covered, and the tier guards were added. Co-Authored-By: Claude Opus 5 --- .github/workflows/Release.yml | 10 + .github/workflows/buildDev.yml | 8 + Tests.Core.Windows/Tests.Core.Windows.csproj | 30 ++ Tests.Core.Windows/TierBoundaryTests.cs | 63 +++ Tests.Core.Windows/Usings.cs | 1 + Text-Grab.Core.Windows/AssemblyInfo.cs | 5 + Text-Grab.Core/AssemblyInfo.cs | 3 + Text-Grab.sln | 18 + docs/Core-Split-Plan.md | 417 +++++++++++++++++++ 9 files changed, 555 insertions(+) create mode 100644 Tests.Core.Windows/Tests.Core.Windows.csproj create mode 100644 Tests.Core.Windows/TierBoundaryTests.cs create mode 100644 Tests.Core.Windows/Usings.cs create mode 100644 Text-Grab.Core.Windows/AssemblyInfo.cs create mode 100644 docs/Core-Split-Plan.md diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 5132f8d0..7cd85d4b 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -24,6 +24,8 @@ env: PROJECT: 'Text-Grab' PROJECT_PATH: 'Text-Grab/Text-Grab.csproj' TEST_PATH: 'Tests/Tests.csproj' + TEST_CORE_PATH: 'Tests.Core/Tests.Core.csproj' + TEST_CORE_WINDOWS_PATH: 'Tests.Core.Windows/Tests.Core.Windows.csproj' BUILD_X64: 'bld/x64' BUILD_X64_SC: 'bld/x64/Text-Grab-Self-Contained' BUILD_ARM64: 'bld/arm64' @@ -50,6 +52,14 @@ jobs: - name: Install dependencies run: dotnet restore ${{ env.PROJECT_PATH }} + # The pure tier runs first: it needs no display and finishes in about a second, so a + # logic regression fails the job before the slow WPF/STA suite starts. + - name: Run Core tests + run: dotnet test --project ${{ env.TEST_CORE_PATH }} + + - name: Run Core.Windows tests + run: dotnet test --project ${{ env.TEST_CORE_WINDOWS_PATH }} -r win-x64 + - name: Run tests run: dotnet test --project ${{ env.TEST_PATH }} -r win-x64 diff --git a/.github/workflows/buildDev.yml b/.github/workflows/buildDev.yml index fee1c509..192dec9d 100644 --- a/.github/workflows/buildDev.yml +++ b/.github/workflows/buildDev.yml @@ -12,6 +12,8 @@ concurrency: env: PROJECT_PATH: "Text-Grab/Text-Grab.csproj" TEST_PATH: "Tests/Tests.csproj" + TEST_CORE_PATH: "Tests.Core/Tests.Core.csproj" + TEST_CORE_WINDOWS_PATH: "Tests.Core.Windows/Tests.Core.Windows.csproj" # Unlock token for the Windows AI language model (a Limited Access Feature). Absent secrets # leave the properties empty, which just builds without on-device text AI rather than failing. LAF_TOKEN: ${{ secrets.LAF_TOKEN }} @@ -30,6 +32,12 @@ jobs: run: dotnet restore ${{ env.PROJECT_PATH }} - name: Build run: dotnet build ${{ env.PROJECT_PATH }} -p:EnableMsixTooling=true + # The pure tier runs first: it needs no display and finishes in about a second, so a + # logic regression fails the job before the slow WPF/STA suite starts. + - name: Test Core + run: dotnet test --project ${{ env.TEST_CORE_PATH }} + - name: Test Core.Windows + run: dotnet test --project ${{ env.TEST_CORE_WINDOWS_PATH }} -r win-x64 - name: Test run: dotnet test --project ${{ env.TEST_PATH }} -r win-x64 diff --git a/Tests.Core.Windows/Tests.Core.Windows.csproj b/Tests.Core.Windows/Tests.Core.Windows.csproj new file mode 100644 index 00000000..e1fee099 --- /dev/null +++ b/Tests.Core.Windows/Tests.Core.Windows.csproj @@ -0,0 +1,30 @@ + + + + net10.0-windows10.0.22621.0 + 10.0.22621.48 + Exe + Text_Grab.Tests.Core.Windows + enable + enable + false + + x64;x86;ARM64 + win-x86;win-x64;win-arm64 + + false + false + + + + + + + + + + + diff --git a/Tests.Core.Windows/TierBoundaryTests.cs b/Tests.Core.Windows/TierBoundaryTests.cs new file mode 100644 index 00000000..3af93948 --- /dev/null +++ b/Tests.Core.Windows/TierBoundaryTests.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Reflection; +using Text_Grab.Models; + +namespace Text_Grab.Tests.Core.Windows; + +/// +/// Structural guards on the Core tier. These are cheap and they fail loudly the moment a move +/// smuggles a WPF dependency into a library that is supposed to be headless - which otherwise +/// only shows up much later, as an unexplained UseWPF flip in a csproj diff. +/// +public class TierBoundaryTests +{ + private static readonly Assembly CoreAssembly = typeof(RectangleFExtensions).Assembly; + private static readonly Assembly CoreWindowsAssembly = typeof(IOcrLinesWords).Assembly; + + [Fact] + public void TextGrabCore_ReferencesNoWpfOrWinRtAssemblies() + { + string[] offenders = ReferencedAssemblyNames(CoreAssembly) + .Where(IsUiOrWindowsAssembly) + .ToArray(); + + Assert.True( + offenders.Length == 0, + $"Text-Grab.Core must stay platform-neutral but references: {string.Join(", ", offenders)}"); + } + + [Fact] + public void TextGrabCoreWindows_ReferencesNoWpfAssemblies() + { + // Windows APIs are expected here; WPF is not. Core.Windows keeps UseWPF=false so that the + // OCR, capture and interop code stays usable from a headless host. + string[] offenders = ReferencedAssemblyNames(CoreWindowsAssembly) + .Where(IsWpfAssembly) + .ToArray(); + + Assert.True( + offenders.Length == 0, + $"Text-Grab.Core.Windows must not use WPF but references: {string.Join(", ", offenders)}"); + } + + [Fact] + public void TextGrabCore_DoesNotReferenceTextGrabCoreWindows() + { + // Dependencies point one way: app -> Core.Windows -> Core. + Assert.DoesNotContain( + "Text-Grab.Core.Windows", + ReferencedAssemblyNames(CoreAssembly)); + } + + private static string[] ReferencedAssemblyNames(Assembly assembly) + => [.. assembly.GetReferencedAssemblies().Select(static name => name.Name ?? string.Empty)]; + + private static bool IsWpfAssembly(string name) + => name is "PresentationCore" or "PresentationFramework" or "WindowsBase" or "System.Xaml"; + + private static bool IsUiOrWindowsAssembly(string name) + => IsWpfAssembly(name) + || name is "System.Windows.Forms" or "System.Drawing.Common" + || name.StartsWith("Microsoft.Windows.", System.StringComparison.Ordinal) + || name.StartsWith("Microsoft.WindowsAppSDK", System.StringComparison.Ordinal); +} diff --git a/Tests.Core.Windows/Usings.cs b/Tests.Core.Windows/Usings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/Tests.Core.Windows/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/Text-Grab.Core.Windows/AssemblyInfo.cs b/Text-Grab.Core.Windows/AssemblyInfo.cs new file mode 100644 index 00000000..2d361181 --- /dev/null +++ b/Text-Grab.Core.Windows/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Text-Grab")] +[assembly: InternalsVisibleTo("Tests")] +[assembly: InternalsVisibleTo("Tests.Core.Windows")] diff --git a/Text-Grab.Core/AssemblyInfo.cs b/Text-Grab.Core/AssemblyInfo.cs index 6695779d..01353f25 100644 --- a/Text-Grab.Core/AssemblyInfo.cs +++ b/Text-Grab.Core/AssemblyInfo.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Text-Grab")] +[assembly: InternalsVisibleTo("Text-Grab.Core.Windows")] [assembly: InternalsVisibleTo("Tests")] +[assembly: InternalsVisibleTo("Tests.Core")] +[assembly: InternalsVisibleTo("Tests.Core.Windows")] diff --git a/Text-Grab.sln b/Text-Grab.sln index 26d05451..71716c68 100644 --- a/Text-Grab.sln +++ b/Text-Grab.sln @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Text-Grab.Core.Windows", "T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Core", "Tests.Core\Tests.Core.csproj", "{F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Core.Windows", "Tests.Core.Windows\Tests.Core.Windows.csproj", "{5F8E212A-3C21-46EF-97EF-7B890C112873}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -152,6 +154,22 @@ Global {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|x86.Build.0 = Release|Any CPU {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|Any CPU.Build.0 = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|ARM64.Build.0 = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x64.Build.0 = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x86.Build.0 = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|ARM64.ActiveCfg = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|ARM64.Build.0 = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x64.ActiveCfg = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x64.Build.0 = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x86.ActiveCfg = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x86.Build.0 = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md new file mode 100644 index 00000000..d5b2b898 --- /dev/null +++ b/docs/Core-Split-Plan.md @@ -0,0 +1,417 @@ +# Core Split Plan + +Reorganizing Text-Grab from one 178-file WPF app into a layered set of projects: + +``` +Text-Grab.Core net10.0 pure logic, no UI, no Windows + ^ +Text-Grab.Core.Windows net10.0-windows10.0.22621.0 WinRT / GDI+ / P-Invoke, UseWPF=false + ^ +Text-Grab net10.0-windows... WPF app Views, Controls, Pages, app wiring +``` + +Test projects mirror the same tiers: `Tests.Core` (net10.0, fast), `Tests.Core.Windows` +(to be created), `Tests` (net10.0-windows, WPF/STA, references the app). + +Phase 0 (scaffolding) and the first six move commits are already done — see +`git log --oneline` from `288f6d1` forward. This document is the plan for the rest and +the standing contract for every agent that works on it. + +--- + +## 1. Invariants — every agent must follow these + +1. **All three projects share `RootNamespace = Text_Grab`.** A moved file keeps its + `namespace` line unchanged. A move is `git mv` plus fixing only what actually breaks. + Never "tidy" namespaces during a move. +2. **`Text-Grab.Core.Windows` keeps `UseWPF=false` and `UseWindowsForms=false`.** If a file + needs `System.Windows.*` or `System.Windows.Forms.*`, it either stays in the app or gets + split. Do not flip these flags to make a move work. +3. **Dependencies point one way only:** app → Core.Windows → Core. Core never references + Core.Windows; neither library ever references the app. +4. **One batch = one commit.** Do not start the next batch until the current one builds. +5. **Defer, don't redesign.** If a file in your list turns out to be blocked by something + outside your list, leave it, record it in §7 (Deferred ledger) with the specific blocker, + and move on. Do not expand scope to unblock it. +6. **Never run two edit-capable agents against this working tree at once.** Moves touch + shared files (`.csproj`, call sites, `Enums.cs`); concurrent edits corrupt each other. + Reconnaissance agents (read-only) may run in parallel; movers run serially. +7. Commit messages follow the established style: what moved, what had to be fixed and why, + what was deferred and its specific blocker. See `edefeaa` and `e677b54` for the pattern. + +## 2. Verification gates — exact commands + +```bash +# Primary gate. Builds Core -> Core.Windows -> app -> Tests. ~13s incremental. +dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 + +# Fast library gate, run first while iterating. +dotnet build Tests.Core/Tests.Core.csproj -c Debug + +# Wave boundaries. Run the two fast ones first - they need no display and finish in ~1s each. +dotnet test --project Tests.Core/Tests.Core.csproj +dotnet test --project Tests.Core.Windows/Tests.Core.Windows.csproj -p:Platform=x64 +dotnet test --project Tests/Tests.csproj -r win-x64 # slow, spawns STA/WPF tests +``` + +`Tests.Core.Windows` needs `-p:Platform=x64` (or another named platform). Without it the +Windows App SDK targets, pulled in transitively via `Text-Grab.Core.Windows`, fail with +`WindowsAppSDKSelfContained requires a supported Windows architecture`. + +**Do not run `dotnet build Text-Grab.sln`.** The MSIX `Text-Grab-Package.wapproj` fails +under the dotnet CLI with `MSB4019: Microsoft.DesktopBridge.props was not found` — it needs +full MSBuild / Visual Studio. That error is pre-existing and unrelated to this work; the +per-project builds above are the real gate. + +Also note: `Text-Grab.Core` and `Text-Grab.Core.Windows` must keep +`win-x86;win-x64;win-arm64`. The wapproj restores +project references per-RID and drops `NETSDK1047` without them. + +## 3. Wave 0 — the three foundation decisions + +These gate almost everything else and are design work, not mechanical moves. **Do these +yourself (Opus), serially, before dispatching any Sonnet mover.** + +### B1 — Settings access + +48 app files reach settings through `AppUtilities.TextGrabSettings`, which returns +`Text_Grab.Properties.Settings` — an `internal sealed partial class : ApplicationSettingsBase` +with 104 generated properties. Only **28 distinct properties** are actually read through that +accessor, so the real coupling surface is small. + +**Done in `8398af2`:** `Text-Grab.Core/Interfaces/ITextGrabSettings.cs` declares the slice +portable code may read; `Text-Grab.Core/Services/SettingsAccess.cs` resolves it. Core code reads +`SettingsAccess.Current.CorrectToLatin` instead of `AppUtilities.TextGrabSettings.CorrectToLatin`. + +The app implements the interface by *declaring* it — the generated properties already match on +name and type, and `Save()` comes from `ApplicationSettingsBase`, so there is no forwarding code: + +```csharp +// Text-Grab/Properties/Settings.cs — existing hand-written partial +[SettingsProvider(typeof(AutomationSettingsProvider))] +internal sealed partial class Settings : ITextGrabSettings { } +``` + +`SettingsAccess` holds a `Func` rather than an instance, because the app's +settings object hangs off `Singleton.Instance`, which is lazy and does real work +on first touch. The app registers it from a `[ModuleInitializer]`, not from `App.appStartup`: the +`Tests` host loads the app assembly and runs its code without ever raising the WPF Startup event. + +**Seeded properties** (from the actual near-term consumers, not guessed): `CorrectErrors`, +`CorrectToLatin`, `ParagraphDetection`, `RemoveFurigana`, `TryToReadBarcodes`, +`UiAutomationFallbackToOcr`, `UseTesseract`, `TesseractPath`, `LastUsedLang`, `Save()`. + +**Adding a property:** add it to the interface. If the build then fails, the missing property +belongs in `Settings.settings` — do not write a forwarding property in the partial. If one move +would need more than a handful of new members, use the façade split instead. + +**Prefer the cheaper alternative for leaf cases:** when a file has exactly one settings +touchpoint, split the pure part into Core and leave a thin settings-reading façade in the app. +That is what `e677b54` did with `PatternItem` / `PatternItemCatalog`, and it stays the right +move for one-off cases. Use the interface when a file has many touchpoints or the façade +would be larger than the thing it wraps. + +### B2 — Geometry currency + +`System.Windows.Rect` / `Point` / `Size` live in `WindowsBase.dll`, which only comes with +`UseWPF=true`. They appear in ~30 otherwise-portable files and are the single most common +blocker after settings. + +**Done in `30af90f`:** `System.Drawing.RectangleF` / `PointF` / `SizeF` are the geometry type in +Core and Core.Windows. These are in **System.Drawing.Primitives**, part of the shared framework — +genuinely cross-platform, needs no Windows TFM and no package reference. +`Text-Grab.Core/Utilities/RectangleFExtensions.cs` carries the portable helpers (`IsGood`, +`CenterPoint`, `GetScaledUpByFraction`, `GetScaleSizeByFraction`, `Union`); the app's existing +`Extensions/ShapeExtensions.cs` gained the boundary conversions (`AsRect` / `AsRectangleF`, +`AsPoint` / `AsPointF`, `AsSize` / `AsSizeF`) alongside the `Rectangle` ↔ `Rect` pair it already +had. View code converts at the edge. + +> Important distinction agents keep getting wrong: `System.Drawing.Primitives` (Rectangle, +> RectangleF, Point, PointF, Size, SizeF, Color) is portable and fine in **Core**. +> `System.Drawing.Common` (Bitmap, Graphics, Icon, BitmapData) is Windows-only and belongs in +> **Core.Windows**. A `using System.Drawing;` alone tells you nothing — check which types. + +### B3 — Imaging currency + +Movable code juggles four bitmap representations. The rule: + +| Type | Assembly | Allowed in | +|---|---|---| +| `System.Drawing.Bitmap` | System.Drawing.Common | Core.Windows, app | +| `Windows.Graphics.Imaging.SoftwareBitmap` | WinRT | Core.Windows, app | +| `ImageMagick.MagickImage` | Magick.NET | Core.Windows, app | +| `System.Windows.Media.Imaging.BitmapSource` | WPF | **app only** | +| `byte[]` / `Stream` | — | Core | + +`BitmapSource` never crosses out of the app. Core (pure) traffics in `byte[]`/`Stream`. + +### Wave 0 batches — **done** + +| Batch | Work | Commit | +|---|---|---| +| 0a | B2 geometry currency | `30af90f` | +| 0b | B1 settings seam | `8398af2` | +| 0c | Test scaffolding, tier guards, CI | this commit | + +Three things came out differently than planned above; the text has been corrected to match +what was actually built: + +- **0a extended `ShapeExtensions` instead of adding `WpfGeometryExtensions`.** That file already + carried `Rectangle` ↔ `Rect`, so the conversions belonged next to it rather than in a parallel + API. Core got `RectangleFExtensions` mirroring the portable helpers. +- **0b registers the resolver from a `[ModuleInitializer]`, not `App.appStartup`.** The `Tests` + host loads the app assembly and exercises its code without ever raising the WPF Startup event, + so wiring it at startup would have left every app-referencing test unable to read settings. +- **0c added `Tests.Core.Windows/TierBoundaryTests.cs`**, which was not in the original plan. + It asserts by reflection that Core references no WPF/WinRT assembly, that Core.Windows + references no WPF assembly, and that Core does not reference Core.Windows. This is the + automated enforcement of invariants 2 and 3 — cheaper than catching a `UseWPF` flip in review. + +## 4. Wave plan + +Each batch is one commit, gated on §2. File lists are starting points — an agent that finds a +listed file blocked defers it per invariant 5. + +### Wave 1 — pure leaves → Text-Grab.Core + +**1a — Models (14 files).** `AsyncOcrFileResult`, `EditTextTableDocument`, `ExtractedPattern`, +`FindResult`, `GrabFrameTableEditState`, `GrabFrameWordGroupingMode`, `GrabTemplate`, +`LookupItem`, `NullAsyncResult`, `OcrDirectoryOptions`, `SpreadsheetUndoHistory`, +`TemplatePatternMatch`, `TemplateRecognizerMatch`, `ThirdPartyPackageInfo`. +All verified dependency-free. Expect a near-pure `git mv` batch. + +**1b — Utilities, Extensions, Interfaces (14 files).** `Utilities/PatternExecutor`, +`ColumnSplitUtilities`, `NumericUtilities`, `LanguageHeuristics`, `ProtocolUtilities`, +`Singleton`, `StreamWrapper`, `Json`, `ThirdPartyNoticeUtilities`; +`Extensions/NumberExtensions`, `StringBuilderExtensions`; `Interfaces/ITtsEngine`; +`UndoRedoOperations/UndoRedo`, `ChangeWord`, `ResizeWordBorder`. +Note `Json.cs` is in namespace `Text_Grab.Helpers`, not `Text_Grab.Utilities` — keep it. + +**1c — Enums consolidation.** `Text-Grab/Enums.cs` (132 lines) vs `Text-Grab.Core/Enums.cs` +(15 lines). Move every enum with no UI/Windows dependency into Core's file; leave WPF-typed +ones behind. Verify no duplicate definitions across the two assemblies. + +### Wave 2 — calculation and text engine → Text-Grab.Core + +**2a — CalculationService (3 files, ~2000 lines).** `CalculationService.cs`, +`.UnitMath.cs`, `.DateTimeMath.cs` are fully pure (NCalc + UnitsNet only). Move the +`NCalcAsync` and `UnitsNet` `PackageReference` entries into `Text-Grab.Core.csproj`; they can +stay in the app csproj too if app code still uses them directly — check before deleting. +High-value batch: unlocks `CalculatorTests` and `UnitConversionTests` for `Tests.Core`. + +**2b — Markdown split.** `MarkdownDocumentUtilities.cs` (1168 lines) mixes Markdig AST +parsing (portable) with `FlowDocument`/`System.Windows.Documents` rendering (WPF). Split into +`Text-Grab.Core/Utilities/MarkdownParsing.cs` and keep the FlowDocument half in the app. +Move the `Markdig` package reference to Core. + +### Wave 3 — Windows leaves → Text-Grab.Core.Windows + +**3a — Registry and interop.** `Utilities/ContextMenuUtilities`, `FileAssociationUtilities`, +`RegistryMonitor` (all `Microsoft.Win32.Registry`); `NativeMethods.cs`; `OSInterop.cs` +(1292 lines, `System.Windows.Forms` reference — verify whether it is a real WinForms +dependency or just a `using`; if real, this file stays in the app). + +**3b — WinRT storage and language leaves.** `Extensions/StorageFileExtensions`, +`SettingsStorageExtensions`, `SoftwareBitmapExtensions`; `Models/GeneratedOcrLinesWords`, +`UiAutomationLang`, `WindowsAiLang`, `WindowsAiDescriptionLang`; +`Extensions/LanguageExtensions` (tagged WPF — check whether that is load-bearing). + +**3c — Windows AI.** `Utilities/LimitedAccessFeatureUtilities`, `WinAiLanguageModel` (606), +`WinAiTranslator` (498), `WinAiMeetingNotes`, `ImplementAppOptions`, +`DesktopNotificationManagerCompat` (512). `WindowsAiUtilities` (408) is settings + GDI+ +coupled — attempt after B1 lands, defer if it drags in more. + +### Wave 4 — the OCR pipeline (the pivotal cut) + +**4a — `OcrUtilities.cs` split (1061 lines). Opus owns this one.** It is the hinge of the +whole reorganization: headless OCR orchestration tangled with WPF adapters, plus a hidden WPF +dependency in `LoadBitmapFromFile`, plus `private static readonly Settings DefaultSettings = +AppUtilities.TextGrabSettings` at class scope. Target shape: +- `Text-Grab.Core.Windows/Ocr/OcrEngine.cs` — engine selection, language handling, + WinRT/Windows-AI/Tesseract dispatch, result assembly. +- `Text-Grab/Utilities/OcrUiAdapters.cs` — everything returning or consuming `BitmapSource`, + `System.Windows.Rect`, `Text_Grab.Controls.*`. +Do this as its own commit with nothing else in it. + +**4b — OCR periphery.** `Models/OcrOutput` (blocks `BarcodeUtilities` — it reads settings +directly inside `CleanOutput()`), `Utilities/BarcodeUtilities`, `PdfDocumentRenderer` (497), +the remainder of `TesseractHelper` (401 — `GetTesseractPath` has a settings-write side effect +that must be lifted out first). + +**4c — Language services and tables.** `Services/LanguageService` (392), +`Utilities/CaptureLanguageUtilities`, `LanguageUtilities`, and `Models/ResultTable` (950 — +needs B2 for `System.Windows.Rect` and decoupling from `Windows.Media.Ocr.OcrResult`; consider +having it consume the existing `IOcrLinesWords` from +`Text-Grab.Core.Windows/Models/OcrLinesWords.cs` instead). + +### Wave 5 — capture and imaging → Text-Grab.Core.Windows + +**5a.** `Utilities/ImageMethods` (WPF + WinRT + GDI+ + settings — expect a split, not a move), +`ImageChangeDetector`, `MagickHelpers`, `FileUtilities` (400). + +**5b.** `Utilities/Hdr/HdrToneMapper` (already pure — could go to plain Core), +`Hdr/DisplayHdrInfo`, `Hdr/HdrScreenCapture` (504), `FreeformCaptureUtilities`, +`CameraCaptureUtilities`, `ClipboardUtilities` (464, WinForms clipboard — likely stays), +`Models/DragDataObject`. + +### Wave 6 — services and settings + +**6a.** `Services/SettingsService` (836), `Utilities/AutomationProfile`, +`AutomationSettingsProvider`, `SettingsImportExportUtilities` (435). `AutomationSettingsProvider` +derives from `LocalFileSettingsProvider` (System.Configuration) — that package works on +net10.0, so plain Core is plausible; verify before assuming. + +**6b.** `Services/HistoryService` (1004), `Utilities/GrabTemplateManager`, +`GrabTemplateExecutor` (628), `PostGrabActionManager`, `CustomBottomBarUtilities`, +`ShortcutKeysUtilities`. + +**6c.** `Services/TtsService`, `WindowsSpeechEngine`, `Utilities/AudioTranscriptionUtilities` +(1115), `UIAutomationUtilities` (1362), `DiagnosticsUtilities` (670). + +### Wave 7 — tests and closeout + +**7a — test migration.** Move to `Tests.Core`: `StringMethodTests`, +`TextSearchUtilitiesTests`, `RecognizerExecutorTests`, `PatternExecutorTests`, +`CalculatorTests`, `UnitConversionTests`, `ExtractedPatternTests`, +`ColumnSplitUtilitiesTests`, `SpreadsheetUndoHistoryTests`, `EditTextTableDocumentTests`, +`GrabFrameTableEditStateTests`, `ThirdPartyNoticeUtilitiesTests`, `ProtocolUtilitiesTests`, +plus whatever else became pure. Move Windows-but-headless tests into `Tests.Core.Windows`. +Delete `Tests.Core/ScaffoldingSmokeTests.cs` once real tests exist. + +**7b — closeout.** Remove dead app-side shims; update `.github/workflows/*.yml` to run all +three test projects; confirm the MSIX package still builds in Visual Studio (the one thing the +CLI gate cannot check); update `BUILT-WITH.md` / this document with the final layer map. + +### Never moves + +`Views/`, `Controls/`, `Pages/`, `Styles/`, `Themes/`, `App.xaml.cs`, `AssemblyInfo.cs`, +`WPFExtensionMethods.cs`, `Properties/Settings.Designer.cs`, `Extensions/ControlExtensions`, +`DapploExtensions`, `KeyboardExtensions`, `ShapeExtensions`, `Utilities/ColorHelper`, +`CursorClipper`, `GrabFrameViewScaleUtilities`, `NotificationUtilities`, +`WindowSelectionUtilities`, `WindowResizer`, `WindowUtilities`, `HotKeyManager`, +`AutomationDiagnostics`, `NotifyIconUtilities`, `OutputUtilities`, `ShareTargetUtilities`, +and the WPF-typed `UndoRedoOperations` (`AddWordBorder`, `RemoveWordBorder`, `ChangedImage`, +`Operation`) and Models (`FullscreenCaptureResult`, `PostGrabContext`, `ShortcutKeySet`, +`TemplateRegion`, `UiAutomationOptions`, `UiAutomationOverlayItem`, +`UiAutomationOverlaySnapshot`, `WindowSelectionCandidate`, `WordBorderInfo`, `ButtonInfo` — +the last uses `Wpf.Ui.Controls`). + +Several of the Models above become movable once B2 lands. Revisit them in Wave 7 rather than +guessing early. + +## 5. Sub-agent orchestration + +### Roles + +| Role | Model | Isolation | Parallel? | +|---|---|---|---| +| **Cartographer** — read-only dependency mapping of one area | Sonnet, `Explore` | none (read-only) | **yes**, 4–6 at once | +| **Mover** — executes one batch, commits it | Sonnet, `general-purpose` | none (main tree) | **no**, strictly serial | +| **Architect** — Wave 0, batch 4a, any split that changes a public shape | Opus (you) | none | n/a | +| **Verifier** — build + test at wave boundaries | Sonnet | none | no | + +### Why movers are serial + +Every batch touches shared state: `.csproj` `PackageReference` lists, `Enums.cs`, and call +sites in `EditTextWindow.xaml.cs` (7799 lines) and `GrabFrame.xaml.cs` (6442 lines) that +almost every batch edits. Worktree isolation would just relocate the conflict to a merge that +is harder to resolve than the original edit. Serial movers with a 13-second build gate between +them is the faster path in wall-clock terms. + +The parallelism worth having is in reconnaissance: dispatch cartographers for Waves 3–6 +simultaneously while you do Wave 0, so every mover starts with an accurate file list. + +### Mover prompt template + +``` +You are executing batch of the Text-Grab Core split. + +Read D:\source\TheJoeFin\Text-Grab\docs\Core-Split-Plan.md first — sections 1, 2, and +your batch in section 4 are binding. Then read the two most recent move commits +(git show edefeaa, git show e677b54) to match the established style. + +Your file list is exactly: + +Target project: + +Procedure, per file: + 1. Read it. Confirm its actual dependencies — do not trust `using` lines alone. + `System.Drawing` primitives (RectangleF/PointF/SizeF) are portable and fine in Core; + `System.Drawing` GDI+ (Bitmap/Graphics/Icon) is Core.Windows only. + 2. If it moves cleanly: `git mv` it, keep the namespace, fix call sites the compiler flags. + 3. If it needs a split: pure part moves, the coupled façade stays in the app under a new + name. See PatternItem/PatternItemCatalog in e677b54 for the shape. + 4. If it is blocked by something outside your list: LEAVE IT. Do not expand scope. + +After each file, run: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 +Never run `dotnet build Text-Grab.sln` — the wapproj fails under the dotnet CLI by design. + +When the whole list is done and the build is clean: + - Append every deferred file to section 7 of Core-Split-Plan.md with its specific blocker. + - Commit everything as one commit in the established style. + +Report back: files moved, files split (and how), files deferred (and why), final build status. +Do not report success unless the build actually succeeded — paste the failure if it did not. +``` + +### Cartographer prompt template + +``` +Read-only reconnaissance for the Text-Grab Core split. Make no edits. + +Area: + +For each file report: + - Its real dependency set: WPF types, WinForms types, WinRT namespaces, System.Drawing + primitives vs GDI+, P/Invoke, settings access, and which other Text-Grab types it needs. + - Verdict: moves clean to Core / moves clean to Core.Windows / needs a split (say where the + seam is) / stays in the app (say why). + - Which OTHER files a move would drag in. +Rank the area into tiers: move-now, move-after-B1-settings, move-after-B2-geometry, never. +Be specific about blockers — "uses settings" is useless; "GetTesseractPath writes +TesseractPath back to settings as a side effect" is what I need. +``` + +### Cadence + +Do not fire-and-forget the whole chain. Run **one wave at a time**, and between waves: +`git log --oneline -8`, run the wave-boundary test commands, and skim the batch diffs. The +prior six commits were all human-reviewed; that ratio should hold — a mover that silently +"fixes" a call site incorrectly compiles fine and breaks at runtime. + +## 6. Risk register + +| Risk | Mitigation | +|---|---| +| MSIX packaging breaks (CLI gate can't see it) | Open the solution in VS and build the wapproj at each wave boundary. | +| A mover flips `UseWPF=true` on Core.Windows to unblock itself | `Tests.Core.Windows/TierBoundaryTests.cs` fails the build. Also check the csproj diff in review. | +| Silent behavior change from a "cleanup" during a move | Movers are told to change only what the compiler flags. Review diffs for unrequested edits. | +| `RuntimeIdentifiers` dropped from a library csproj | Causes NETSDK1047 in the wapproj restore only — invisible to the CLI gate. Grep the csprojs at wave boundaries. | +| Settings interface sprawls to all 104 properties | Add properties only when a move demands one; if a batch needs more than ~5 new ones, that file probably wants a façade split instead. | +| `EditTextWindow.xaml.cs` / `GrabFrame.xaml.cs` churn | They are touched by most batches. Serial movers make this safe; parallel ones would not. | + +## 7. Deferred ledger + +Files attempted and left behind, with the specific blocker. **Movers append here.** + +| File | Blocker | Unblocked by | +|---|---|---| +| `Utilities/OcrUtilities.cs` | 1061 lines mixing headless orchestration with WPF adapters; hidden WPF dep in `LoadBitmapFromFile`; class-level `AppUtilities.TextGrabSettings` field | Batch 4a (Opus) | +| `Utilities/TesseractHelper.cs` (class) | `GetTesseractPath()` writes back to settings as a side effect | B1 + lifting the write out | +| `Services/LanguageService.cs` | settings + WPF coupled; drags in several untouched types | B1, batch 4c | +| `Utilities/WindowsAiUtilities.cs` | settings + WPF coupled; drags in several untouched types | B1, batch 3c | +| `Utilities/BarcodeUtilities.cs` | `OcrOutput.CleanOutput()` reads settings directly | B1, batch 4b | +| HDR / WGC capture (`Utilities/Hdr/*`) | separate area, mostly clean already | batch 5b | + +## 8. Definition of done + +- `Text-Grab.Core` holds the text, pattern, table, calculation, and template logic, with no + `System.Windows`, no `Windows.*`, no P/Invoke. +- `Text-Grab.Core.Windows` holds OCR engines, capture, imaging, Windows AI, and Win32 interop, + with `UseWPF=false` still set. +- `Text-Grab` holds Views, Controls, Pages, app wiring, and thin adapters — nothing else. +- `Tests.Core` runs in ~2s with no display and covers the pure tier; `Tests` keeps only the + WPF/STA tests. +- CI runs all three test projects; the MSIX package builds in Visual Studio. +- Section 7 is empty, or every remaining row has a written reason it stays. From cd247499430bacc31d668a7e3c723c2a845e8aa8 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 00:05:48 -0500 Subject: [PATCH 11/54] Rebuild the Core split plan from five reconnaissance passes Wave 0 is done and the plan's remaining file lists were unreliable, so this replaces them with lists derived from reading every candidate file end-to-end rather than from grepping using directives. The original lists were wrong about roughly a dozen files in both directions, all for the same four reasons - now written down as invariant 8, because they will keep recurring: - A file with no System.Windows using can still be WPF-bound. UndoRedo.cs, ChangeWord.cs and ResizeWordBorder.cs reach WPF through "using Text_Grab.Controls;", because WordBorder IS a WPF Control. - A fully-qualified type appears in no using at all. LookupItem.cs holds a Wpf.Ui.Controls.SymbolRegular property. - System.Drawing is two things: portable primitives vs Windows-only GDI+. - Rect is two things: Windows.Foundation.Rect (WinRT, fine in Core.Windows) vs System.Windows.Rect (WindowsBase, not). edefeaa already hit this once. Those four files leave the wave lists for the never-move list, along with ImplementAppOptions (app lifecycle, casts to the WPF App), MagickHelpers, CameraCaptureUtilities, SettingsImportExportUtilities and DiagnosticsUtilities - the last of which reads ~70 settings properties, fourteen times the threshold at which the plan already says to prefer a facade. Two files move the other way. The never-move list was written before B2 landed, and WordBorderInfo and TemplateRegion are WPF-bound *only* through Rect, which is exactly what RectangleF now solves. WordBorderInfo is already the portable projection of the WordBorder control - that is its whole job - so converting BorderRect and leaving the WordBorderInfo(WordBorder) constructor behind as a factory unblocks ResultTable's clustering algorithm. TemplateRegion likewise unblocks GrabTemplate and OcrDirectoryOptions. New Wave 1, which did not exist before and is now the highest-leverage batch in the plan: a handful of tiny leaves (Singleton, WrappingStream, NullAsyncResult, Json, IoUtilities' pure half, StorageFileExtensions) plus extracting AppUtilities.IsPackaged()/GetAppVersion() into a Core.Windows PackageIdentity. Three separate recon passes independently hit these same files as blockers; no wave owned them. Waves 3, 5 and 6 all stall without them, because Core.Windows cannot reach back into the app. Other corrections worth naming: - OSInterop.cs (1292 lines) is blocked by one dead line. System.Windows.Forms appears exactly once in the file, in a GetAsyncKeyState(Keys) overload with no callers; the live caller uses the int overload. - AutomationProfile and AutomationSettingsProvider go to plain Core, not Core.Windows. Verified by building and running a probe: ApplicationSettingsBase and LocalFileSettingsProvider resolve and run on net10.0 with a System.Configuration.ConfigurationManager package reference. AutomationProfile needs only ApplySeed widened to take ApplicationSettingsBase. - ResultTable's OcrResult coupling is dead code, not a blocker; its live path already consumes IOcrLinesWords. - TesseractHelper's settings write-back needs no redesign - ITextGrabSettings.Save() covers it. The real blocker is AutomationProfile via one method. - PdfDocumentRenderer moves to wave 5; its blocker is BitmapSource currency, the same as ImageMethods, not anything OCR-specific. - Batch 4c cannot precede wave 3 - OcrEngine.cs will not compile until the language models land. - Hdr/* was recorded as "mostly clean already". HdrScreenCapture.cs:472 reaches System.Windows.Application.Current.Dispatcher to pump a consent dialog. Section 4.0 records what changed and why. Section 8 is new: five pieces of verified dead code, each with zero call sites, two of which unblock a move outright. Section 7's ledger rows now carry specific blockers with line numbers instead of "settings + WPF coupled". B4 closes a loophole the recon found in invariant 2: FrameworkReference to Microsoft.WindowsDesktop.App resolves System.Windows.Automation without setting UseWPF, which would have let UIAutomationUtilities move. Declining it - it drags WindowsBase in and puts System.Windows.Rect back within reach of Core.Windows, defeating B2. TierBoundaryTests already fails the build on that, since it checks assembly names. Nine ITextGrabSettings additions cover everything movable in the whole plan, taking the interface from 10 members to 19. The Load*/Save* families are not candidates - they are SettingsService methods, and the PatternItemCatalog facade handles them with no interface change. Co-Authored-By: Claude Opus 5 --- docs/Core-Split-Plan.md | 555 ++++++++++++++++++++++++++++++---------- 1 file changed, 415 insertions(+), 140 deletions(-) diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index d5b2b898..63316b04 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -11,12 +11,16 @@ Text-Grab net10.0-windows... WPF app Views, Controls, Pages, ``` Test projects mirror the same tiers: `Tests.Core` (net10.0, fast), `Tests.Core.Windows` -(to be created), `Tests` (net10.0-windows, WPF/STA, references the app). +(net10.0-windows, headless), `Tests` (net10.0-windows, WPF/STA, references the app). -Phase 0 (scaffolding) and the first six move commits are already done — see +Phase 0 (scaffolding), the first six move commits, and Wave 0 (foundations) are done — see `git log --oneline` from `288f6d1` forward. This document is the plan for the rest and the standing contract for every agent that works on it. +**Section 4's file lists were rebuilt from five parallel reconnaissance passes** that read every +candidate file end-to-end. They supersede the original lists, which were derived from grepping +`using` directives and were wrong in roughly a dozen places (§4.0). + --- ## 1. Invariants — every agent must follow these @@ -38,6 +42,19 @@ the standing contract for every agent that works on it. Reconnaissance agents (read-only) may run in parallel; movers run serially. 7. Commit messages follow the established style: what moved, what had to be fixed and why, what was deferred and its specific blocker. See `edefeaa` and `e677b54` for the pattern. +8. **Never classify a file by its `using` directives.** Check which *types* it actually uses. + The original wave lists in this document were built by grepping usings and were wrong about + a dozen files in both directions — see §4.0. The four traps, all of which bit that pass: + - A file with no `System.Windows` using can still be WPF-bound: `using Text_Grab.Controls;` + reaches `WordBorder`, which *is* a WPF `Control`. + - A fully-qualified type never appears in a using at all + (`Wpf.Ui.Controls.SymbolRegular` in `LookupItem`). + - `System.Drawing` is two different things. Primitives (`RectangleF`, `PointF`, `SizeF`, + `Color`) are portable and fine in **Core**; GDI+ (`Bitmap`, `Graphics`, `Icon`) is + Windows-only and belongs in **Core.Windows**. + - `Rect` is two different things. `Windows.Foundation.Rect` is WinRT and fine in + Core.Windows; `System.Windows.Rect` is WindowsBase and is not. `edefeaa` already hit + this once. ## 2. Verification gates — exact commands @@ -145,6 +162,25 @@ Movable code juggles four bitmap representations. The rule: `BitmapSource` never crosses out of the app. Core (pure) traffics in `byte[]`/`Stream`. +Note that `Magick.NET` splits the same way its consumers do: `Magick.NET.SystemDrawing` is a +plain net8.0 library and is Core.Windows-eligible, while `Magick.NET.SystemWindowsMedia` is +WPF-only and must stay in the app. + +### B4 — UI Automation: the `FrameworkReference` loophole is closed + +`System.Windows.Automation` (`UIAutomationClient.dll`) lives in the WindowsDesktop shared +framework. It can be resolved with `` +*without* setting `UseWPF=true`, which is a real gap in the wording of invariant 2. + +**Decision: do not take it.** That reference also drags in `WindowsBase`, which puts +`System.Windows.Rect` back within reach of Core.Windows and quietly defeats B2. The one file this +affects, `UIAutomationUtilities.cs`, stays in the app — its type surface is four never-move models +deep, which is not worth weakening the tier boundary to move one file. + +`Tests.Core.Windows/TierBoundaryTests.cs` already enforces this: it checks referenced assembly +*names*, and `WindowsBase` is on its list, so the loophole fails the build rather than passing +review. + ### Wave 0 batches — **done** | Batch | Work | Commit | @@ -169,136 +205,322 @@ what was actually built: ## 4. Wave plan -Each batch is one commit, gated on §2. File lists are starting points — an agent that finds a -listed file blocked defers it per invariant 5. +### 4.0 What reconnaissance changed + +Five read-only passes read every candidate file end-to-end. The corrections that matter: -### Wave 1 — pure leaves → Text-Grab.Core +**Moved *out* of the wave lists (now never-move):** -**1a — Models (14 files).** `AsyncOcrFileResult`, `EditTextTableDocument`, `ExtractedPattern`, -`FindResult`, `GrabFrameTableEditState`, `GrabFrameWordGroupingMode`, `GrabTemplate`, -`LookupItem`, `NullAsyncResult`, `OcrDirectoryOptions`, `SpreadsheetUndoHistory`, +| File | Why the original list was wrong | +|---|---| +| `UndoRedoOperations/UndoRedo.cs`, `ChangeWord.cs`, `ResizeWordBorder.cs` | Hold `WordBorder` fields and construct WPF operation classes. No `System.Windows` using — they reach WPF through `using Text_Grab.Controls;`. | +| `Models/LookupItem.cs` | `Wpf.Ui.Controls.SymbolRegular UiSymbol`, fully qualified, so no using revealed it. | +| `Utilities/ImplementAppOptions.cs` | Filed under Windows AI; is actually app-lifecycle plumbing that casts to the WPF `App` and calls the never-move `NotifyIconUtilities`. | +| `Utilities/MagickHelpers.cs` | Every public signature is `ImageSource`→`ImageSource`; its `Magick.NET.SystemWindowsMedia` package is WPF-only. | +| `Utilities/CameraCaptureUtilities.cs` | Both failure paths show `Wpf.Ui.Controls.MessageBox`; entry point needs a WPF `Window` for its `hwnd`. | +| `Utilities/SettingsImportExportUtilities.cs` | Orchestration glue over three other services; reflects over the *entire* settings surface by design. | +| `Utilities/DiagnosticsUtilities.cs` | Reads ~70 settings properties — 14× the façade threshold — and aggregates every deferred subsystem. | + +**Moved *into* the wave lists (the never-move list was written before B2 landed):** + +| File | Why it can move now | +|---|---| +| `Models/WordBorderInfo.cs` | Already the portable projection of the `WordBorder` control — flattening it to data is the class's whole job. Its only WPF tie is `Rect BorderRect` → `RectangleF`. The `WordBorderInfo(WordBorder)` constructor stays in the app as a factory. **This unblocks `ResultTable`'s clustering algorithm.** | +| `Models/TemplateRegion.cs` | Same shape: WPF-bound only through `ToAbsoluteRect`/`FromAbsoluteRect` returning `System.Windows.Rect`. **This unblocks `GrabTemplate` and `OcrDirectoryOptions`.** | + +**Re-scoped:** +- `Utilities/PdfDocumentRenderer.cs` moves from Wave 4 to Wave 5. Its blocker is the + `BitmapSource` currency (`RenderPageAsync` *returns* one) — identical to `ImageMethods`, not + OCR-specific. +- `Utilities/AutomationProfile.cs` and `AutomationSettingsProvider.cs` go to **plain Core**, not + Core.Windows. Verified empirically: with a `System.Configuration.ConfigurationManager` package + reference, `ApplicationSettingsBase` / `LocalFileSettingsProvider` resolve *and run* on plain + net10.0. `AutomationProfile` needs one mechanical change — widen + `ApplySeed(Properties.Settings)` to `ApplySeed(ApplicationSettingsBase)`; every member it + touches is on the base class. `AutomationSettingsProvider` needs no changes at all. +- `Utilities/Hdr/HdrToneMapper.cs` goes to **plain Core** — it uses nothing but `System`. + +**Diagnoses corrected:** +- `ResultTable`'s `OcrResult` coupling is *dead code*, not a blocker. Its live path already + consumes the portable `IOcrLinesWords`. The real blocker was `WordBorderInfo` — now resolved. +- `TesseractHelper`'s settings write-back needs no redesign. `ITextGrabSettings.Save()` already + covers it. Its actual blocker is `AutomationProfile`, via one method (`TempImagePath`). +- `Utilities/Hdr/*` was recorded as "mostly clean already". It is not: `HdrScreenCapture.cs:472` + reaches `System.Windows.Application.Current?.Dispatcher` to pump a consent dialog. + +### 4.1 Wave 1 — shared leaves (do this first) + +**This batch did not exist in the original plan and is now the highest-leverage work in it.** +Reconnaissance found the same handful of tiny files blocking Waves 3, 5 and 6 independently. +Until they land, every later batch hits the same wall: Core.Windows cannot reach back into the +app, so a leaf left behind blocks everything above it. + +**1a — shared leaves → Core** (all verified dependency-free): +`Utilities/Singleton.cs`, `Utilities/StreamWrapper.cs` (class `WrappingStream`), +`Models/NullAsyncResult.cs` (`StreamWrapper` constructs it — same commit), +`Utilities/Json.cs` (namespace is `Text_Grab.Helpers`, not `.Utilities` — keep it), +`Utilities/IoUtilities.cs` *(pure string-list half only; the class also has WinForms and +Wpf.Ui `MessageBox` calls that stay behind)*. + +**1b — packaging identity → Core.Windows.** Extract `AppUtilities.IsPackaged()` and +`GetAppVersion()` into `Text-Grab.Core.Windows/Utilities/PackageIdentity.cs`. Both need only +`Windows.ApplicationModel.Package`. They are unreachable today purely because they share a class +with `TextGrabSettings`/`TextGrabSettingsService`, which must stay in the app. Leave `AppUtilities` +forwarding so the ~dozens of existing call sites need not all change at once. + +`StorageFileExtensions.cs` → Core.Windows also belongs here rather than in Wave 3 — it is +dependency-free and `SoftwareBitmapExtensions` needs it. + +**Why this ordering matters:** 1a+1b unblocks, at minimum, `SettingsStorageExtensions` (3b), +`SoftwareBitmapExtensions` (5), `FileAssociationUtilities` (3a), `WinAiLanguageModel` (3c), +`FileUtilities` (5), and `CameraCaptureUtilities`'s `IsPackaged` call. + +### 4.2 Wave 2 — pure leaves → Core + +**2a — Enums.** Merge all 17 enums from `Text-Grab/Enums.cs` into `Text-Grab.Core/Enums.cs`. +Verified: all 17 are plain int/short-backed with no attributes, and there are **zero** name +collisions with Core's existing two. Do this early — it is a shared file every later batch may +otherwise touch. + +**2b — Models (11 files, verified clean):** `AsyncOcrFileResult`, `EditTextTableDocument`, +`ExtractedPattern`, `FindResult` (calls a static on `EditTextTableDocument` — same commit, that +order), `GrabFrameTableEditState`, `GrabFrameWordGroupingMode`, `SpreadsheetUndoHistory`, `TemplatePatternMatch`, `TemplateRecognizerMatch`, `ThirdPartyPackageInfo`. -All verified dependency-free. Expect a near-pure `git mv` batch. - -**1b — Utilities, Extensions, Interfaces (14 files).** `Utilities/PatternExecutor`, -`ColumnSplitUtilities`, `NumericUtilities`, `LanguageHeuristics`, `ProtocolUtilities`, -`Singleton`, `StreamWrapper`, `Json`, `ThirdPartyNoticeUtilities`; -`Extensions/NumberExtensions`, `StringBuilderExtensions`; `Interfaces/ITtsEngine`; -`UndoRedoOperations/UndoRedo`, `ChangeWord`, `ResizeWordBorder`. -Note `Json.cs` is in namespace `Text_Grab.Helpers`, not `Text_Grab.Utilities` — keep it. - -**1c — Enums consolidation.** `Text-Grab/Enums.cs` (132 lines) vs `Text-Grab.Core/Enums.cs` -(15 lines). Move every enum with no UI/Windows dependency into Core's file; leave WPF-typed -ones behind. Verify no duplicate definitions across the two assemblies. - -### Wave 2 — calculation and text engine → Text-Grab.Core - -**2a — CalculationService (3 files, ~2000 lines).** `CalculationService.cs`, -`.UnitMath.cs`, `.DateTimeMath.cs` are fully pure (NCalc + UnitsNet only). Move the -`NCalcAsync` and `UnitsNet` `PackageReference` entries into `Text-Grab.Core.csproj`; they can -stay in the app csproj too if app code still uses them directly — check before deleting. -High-value batch: unlocks `CalculatorTests` and `UnitConversionTests` for `Tests.Core`. - -**2b — Markdown split.** `MarkdownDocumentUtilities.cs` (1168 lines) mixes Markdig AST -parsing (portable) with `FlowDocument`/`System.Windows.Documents` rendering (WPF). Split into -`Text-Grab.Core/Utilities/MarkdownParsing.cs` and keep the FlowDocument half in the app. -Move the `Markdig` package reference to Core. - -### Wave 3 — Windows leaves → Text-Grab.Core.Windows - -**3a — Registry and interop.** `Utilities/ContextMenuUtilities`, `FileAssociationUtilities`, -`RegistryMonitor` (all `Microsoft.Win32.Registry`); `NativeMethods.cs`; `OSInterop.cs` -(1292 lines, `System.Windows.Forms` reference — verify whether it is a real WinForms -dependency or just a `using`; if real, this file stays in the app). - -**3b — WinRT storage and language leaves.** `Extensions/StorageFileExtensions`, -`SettingsStorageExtensions`, `SoftwareBitmapExtensions`; `Models/GeneratedOcrLinesWords`, -`UiAutomationLang`, `WindowsAiLang`, `WindowsAiDescriptionLang`; -`Extensions/LanguageExtensions` (tagged WPF — check whether that is load-bearing). - -**3c — Windows AI.** `Utilities/LimitedAccessFeatureUtilities`, `WinAiLanguageModel` (606), -`WinAiTranslator` (498), `WinAiMeetingNotes`, `ImplementAppOptions`, -`DesktopNotificationManagerCompat` (512). `WindowsAiUtilities` (408) is settings + GDI+ -coupled — attempt after B1 lands, defer if it drags in more. - -### Wave 4 — the OCR pipeline (the pivotal cut) - -**4a — `OcrUtilities.cs` split (1061 lines). Opus owns this one.** It is the hinge of the -whole reorganization: headless OCR orchestration tangled with WPF adapters, plus a hidden WPF -dependency in `LoadBitmapFromFile`, plus `private static readonly Settings DefaultSettings = -AppUtilities.TextGrabSettings` at class scope. Target shape: -- `Text-Grab.Core.Windows/Ocr/OcrEngine.cs` — engine selection, language handling, - WinRT/Windows-AI/Tesseract dispatch, result assembly. -- `Text-Grab/Utilities/OcrUiAdapters.cs` — everything returning or consuming `BitmapSource`, - `System.Windows.Rect`, `Text_Grab.Controls.*`. -Do this as its own commit with nothing else in it. - -**4b — OCR periphery.** `Models/OcrOutput` (blocks `BarcodeUtilities` — it reads settings -directly inside `CleanOutput()`), `Utilities/BarcodeUtilities`, `PdfDocumentRenderer` (497), -the remainder of `TesseractHelper` (401 — `GetTesseractPath` has a settings-write side effect -that must be lifted out first). - -**4c — Language services and tables.** `Services/LanguageService` (392), -`Utilities/CaptureLanguageUtilities`, `LanguageUtilities`, and `Models/ResultTable` (950 — -needs B2 for `System.Windows.Rect` and decoupling from `Windows.Media.Ocr.OcrResult`; consider -having it consume the existing `IOcrLinesWords` from -`Text-Grab.Core.Windows/Models/OcrLinesWords.cs` instead). - -### Wave 5 — capture and imaging → Text-Grab.Core.Windows - -**5a.** `Utilities/ImageMethods` (WPF + WinRT + GDI+ + settings — expect a split, not a move), -`ImageChangeDetector`, `MagickHelpers`, `FileUtilities` (400). - -**5b.** `Utilities/Hdr/HdrToneMapper` (already pure — could go to plain Core), -`Hdr/DisplayHdrInfo`, `Hdr/HdrScreenCapture` (504), `FreeformCaptureUtilities`, -`CameraCaptureUtilities`, `ClipboardUtilities` (464, WinForms clipboard — likely stays), -`Models/DragDataObject`. - -### Wave 6 — services and settings - -**6a.** `Services/SettingsService` (836), `Utilities/AutomationProfile`, -`AutomationSettingsProvider`, `SettingsImportExportUtilities` (435). `AutomationSettingsProvider` -derives from `LocalFileSettingsProvider` (System.Configuration) — that package works on -net10.0, so plain Core is plausible; verify before assuming. - -**6b.** `Services/HistoryService` (1004), `Utilities/GrabTemplateManager`, -`GrabTemplateExecutor` (628), `PostGrabActionManager`, `CustomBottomBarUtilities`, -`ShortcutKeysUtilities`. - -**6c.** `Services/TtsService`, `WindowsSpeechEngine`, `Utilities/AudioTranscriptionUtilities` -(1115), `UIAutomationUtilities` (1362), `DiagnosticsUtilities` (670). - -### Wave 7 — tests and closeout - -**7a — test migration.** Move to `Tests.Core`: `StringMethodTests`, -`TextSearchUtilitiesTests`, `RecognizerExecutorTests`, `PatternExecutorTests`, -`CalculatorTests`, `UnitConversionTests`, `ExtractedPatternTests`, -`ColumnSplitUtilitiesTests`, `SpreadsheetUndoHistoryTests`, `EditTextTableDocumentTests`, -`GrabFrameTableEditStateTests`, `ThirdPartyNoticeUtilitiesTests`, `ProtocolUtilitiesTests`, -plus whatever else became pure. Move Windows-but-headless tests into `Tests.Core.Windows`. -Delete `Tests.Core/ScaffoldingSmokeTests.cs` once real tests exist. - -**7b — closeout.** Remove dead app-side shims; update `.github/workflows/*.yml` to run all -three test projects; confirm the MSIX package still builds in Visual Studio (the one thing the -CLI gate cannot check); update `BUILT-WITH.md` / this document with the final layer map. - -### Never moves + +**2c — Utilities / Extensions / Interfaces (9 files):** `PatternExecutor` then +`ColumnSplitUtilities` (which calls it — same commit, that order); `NumericUtilities`, +`LanguageHeuristics`, `Extensions/NumberExtensions`, `Extensions/StringBuilderExtensions`, +`Interfaces/ITtsEngine`. + +Two files here need a **split**, not a move: +- `ProtocolUtilities` — only `IsProtocolUri` and `TryParseProtocolUri` are pure. The rest needs + Registry + `AutomationProfile` + `FileUtilities`. +- `ThirdPartyNoticeUtilities` — only `Packages` and the constants are pure. The `Get*Path`/`Open*` + methods need `FileUtilities.GetExePath()`. All three of its existing tests touch only + `Packages`, so the test moves with the pure half. + +**2d — geometry conversions.** Convert `WordBorderInfo.BorderRect` and +`TemplateRegion.ToAbsoluteRect`/`FromAbsoluteRect` to `RectangleF`, move both to Core, leave the +`WordBorderInfo(WordBorder)` factory in the app. Then `GrabTemplate` and `OcrDirectoryOptions` +follow. This is the batch that pays off B2. + +**2e — CalculationService.** All three files, ~2000 lines, fully pure (one call into +`NumericUtilities`). **Move** the `NCalcAsync` and `UnitsNet` package references to +`Text-Grab.Core.csproj` — verified they are used nowhere else in the app. Leave `Tests.csproj`'s +own `NCalcAsync` reference alone. Move `CalculatorTests` and `UnitConversionTests` to `Tests.Core` +in the same commit. + +**2f — Markdown split.** ~150 pure lines out of 1168. Pure half (Markdig AST + regex + string): +`LooksLikeMarkdown`, `ShouldPromoteLiveBlock`, `ShouldPromoteLiveMarkdown`, `NormalizeDocumentText`, +`NormalizeNewlines`, `EscapeMarkdownText`, `EscapeLinkDestination`, `ApplyQuotePrefix`, +`GetQuotePrefix`, `GetOrderedListStart`, `ResolveContentSpan`, `GetSourceSlice`, +`GetCodeSpanContentRawStart`, `GetCodeBlockText`, the `MarkdownPipeline` field and the three +`[GeneratedRegex]` methods. Everything touching `FlowDocument`/`System.Windows.Documents` stays. +The shared string helpers must become `internal`/`public` for the app half to call them. **Markdig +stays referenced by both halves** — the app side pattern-matches on Markdig types directly; verify +transitivity before removing the app's reference. Extract the 6 pure test methods into +`Tests.Core/MarkdownParsingTests.cs`. + +### 4.3 Wave 3 — Windows leaves → Core.Windows + +**3a — eight files, zero blockers, one commit.** `NativeMethods.cs`, `RegistryMonitor.cs` +(namespace is `RegistryUtils`, vendored — keep it), `OSInterop.cs`, +`DesktopNotificationManagerCompat.cs`, `Models/GeneratedOcrLinesWords.cs` (uses +`Windows.Foundation.Rect` — WinRT, not a B2 blocker), `Models/UiAutomationLang.cs`, +`Models/WindowsAiLang.cs`, `Models/WindowsAiDescriptionLang.cs`. + +> `OSInterop.cs` is 1292 lines and was recorded as probably app-bound. `System.Windows.Forms` +> appears **once** in the whole file — in a `GetAsyncKeyState(Keys)` overload with zero callers. +> The only live caller uses the `int` overload. **Delete the dead overload and the file moves.** + +**3b — after Wave 1.** `Extensions/SettingsStorageExtensions.cs` (namespace is `Text_Grab.Helpers` +— keep it; needs `Json.cs` from 1a). `Utilities/LimitedAccessFeatureUtilities.cs` (zero deps — +move first in the WinAI chain). + +**3c — Windows AI chain, in this order:** `WinAiLanguageModel.cs` (needs `PackageIdentity` from 1b, +`OSInterop` from 3a, `LimitedAccessFeatureUtilities` from 3b), then `WinAiTranslator.cs` and +`WinAiMeetingNotes.cs` (both need only `WinAiLanguageModel`). + +**3d — `Extensions/LanguageExtensions.cs` split.** `XmlLanguage` comes from `PresentationCore` +and the path is reachable in production from `GrabFrame`, `EditTextWindow` and `OcrUtilities` — +not a dead using. Move `IsSpaceJoining` (both overloads), `IsLatinBased`, `AsLanguage`, +`AsILanguage`; leave `IsRightToLeft(this Language)` and the `GlobalLang` branch of the `ILanguage` +overload in a thin app-side façade. + +### 4.4 Wave 4 — OCR pipeline + +**4a — `OcrOutput` → `BarcodeUtilities`, a move-now pair.** `OcrOutput.CleanOutput()` needs a +two-line swap to `SettingsAccess.Current` (`CorrectToLatin`, `CorrectErrors` — both already on the +interface) and the `is not Settings userSettings` cast dropped. `BarcodeUtilities` follows +immediately; add `ZXing.Net.Bindings.Windows.Compatibility` to Core.Windows. + +**4b — `TesseractHelper`.** Blocked on `AutomationProfile` (via `TempImagePath` only) — so this +follows Wave 6a. Add `CliWrap` to Core.Windows. `TesseractGitHubFileDownloader` in the same file is +fully portable and could go to plain Core. + +**4c — the `OcrUtilities` split. Opus owns this; do it as its own commit.** +`DefaultSettings` reads exactly six properties, all already on `ITextGrabSettings` — verified, no +seventh. The hidden WPF dependency is `LoadBitmapFromFile` (lines 887–899), which builds a +`BitmapImage` to apply EXIF rotation; decoupling it means a real rewrite against GDI+/WIC, so +**defer that method and its two callers** (`OcrAbsoluteFilePathAsync`, `OcrFile`) rather than +attempt it inside 4c. + +**Ordering constraint the original plan missed: 4c cannot precede Wave 3.** `OcrEngine.cs` will +not compile in Core.Windows until `WindowsAiLang`, `WindowsAiDescriptionLang`, `UiAutomationLang` +(3a), `WinAiLanguageModel` (3c) and `AutomationProfile` (6a) have landed. If those are not ready, +move only the strictly portable subset — furigana filtering, paragraph-wrap heuristics, +`BuildTextFromOcrLines`, `GetStringFromOcrOutputs`, `GetTextFromOcrLine` — and leave engine +dispatch behind. + +`Tests/OcrTests.cs` is the heaviest consumer of the headless surface and becomes a +`Tests.Core.Windows` candidate in Wave 7. It references the nested `PositionedOcrLine` / +`GroupedOcrLines` types by name; both halves keep `Text_Grab.Utilities`, so it stays green. + +**4d — `ResultTable`.** Unblocked by 2d. Delete the dead code first (see §9), then move the +clustering algorithm. + +**4e — language chain, strictly ordered, and hard-blocked at the end.** +`CaptureLanguageUtilities` → `LanguageUtilities` → `LanguageService`. The first two are pure +forwarders and move for free once the third does. `LanguageService` has a genuine blocker: +`System.Windows.Input.InputLanguageManager`, with no portable substitute. It also needs +`UiAutomationEnabled` and `WindowsAiDescriptionEnabled` added to `ITextGrabSettings`. **Route to +Opus** — the workable split is to extract the pure `switch`-expression helpers (`GetLanguageTag`, +`GetLanguageKind`, `GetPersistedLanguageIdentity`, `NormalizePersistedLanguageIdentity`) and leave +the input-language reader in the app. + +### 4.5 Wave 5 — capture and imaging → Core.Windows + +**5a — move-now (after Wave 1):** +- `Utilities/Hdr/HdrToneMapper.cs` → **plain Core** (nothing but `System`). +- `Utilities/Hdr/DisplayHdrInfo.cs` → Core.Windows; add `Vortice.Direct3D11`, `Vortice.DXGI`. +- `Extensions/ImageExtensions.cs` → Core.Windows (pure GDI+; `ExifRotate` is dead — see §9). +- `Utilities/ImageChangeDetector.cs` → Core.Windows; add `Magick.NET-Q16-AnyCPU`, + `Magick.NET.SystemDrawing`. +- `Models/DragDataObject.cs` → Core.Windows, after deleting its dead `BitmapSourceToBitmap` (§9). +- `Extensions/SoftwareBitmapExtensions.cs` → Core.Windows (needs `StorageFileExtensions` and + `WrappingStream` from Wave 1). + +**5b — `HdrScreenCapture.cs`.** The D3D11/DXGI/WinRT pipeline is clean. Two blockers: add +`HdrBorderlessGranted` to `ITextGrabSettings`, and extract the `Application.Current.Dispatcher` +hop at line 472 behind a settable hook the app wires up at startup — it exists to pump a one-time +OS consent dialog and is load-bearing. + +**5c — `ImageMethods.cs` split.** Headless half (→ Core.Windows): `PadImage`, +`CaptureScreenRegion`, `GetBitmapFromIRandomAccessStream`, `GetRotateFlipType(string)`. Everything +touching `BitmapImage`/`BitmapSource`/`CachedBitmap`/`InteropBitmap`/`Window`/`ImageSource` stays. +Add `HdrCaptureCorrection` to `ITextGrabSettings`. **`GetRegionOfScreenAsBitmap` stays behind for +now** — it calls `Singleton.Instance.CacheLastBitmap`, and inverting that call is a +redesign (invariant 5). `GetWindowsBoundsBitmap` is permanently app-bound; it pattern-matches on +the `GrabFrame` *View*. + +**5d — `ClipboardUtilities.cs` split.** Larger than expected in the right direction: ~330 of 464 +lines are a pure CF_HTML table parser with no clipboard, WPF, WinRT or GDI+ dependency → +**plain Core** as `Utilities/CfHtmlTableUtilities.cs`. The clipboard-touching methods stay. +Separately, line 64's `System.Windows.Forms.DataFormats.Bitmap` is the file's only WinForms use +and is the identical string constant to WPF's `System.Windows.DataFormats.Bitmap` — swap it in the +same commit regardless of whether the split happens. + +**5e — `FreeformCaptureUtilities.cs`.** Only `CreateMaskedBitmap` moves, after changing its +parameter from `IReadOnlyList` to `IReadOnlyList`; the single call site in +`FullscreenGrab.SelectionStyles.cs` converts via `AsPointF`. `GetBounds` and `BuildGeometry` +return WPF rendering types (`PathGeometry`) and stay. + +**5f — `PdfDocumentRenderer.cs`** (re-scoped here from Wave 4). Blocked on the same `BitmapSource` +currency as `ImageMethods`: `RenderPageAsync` returns one, and changing that is a public API shape +change affecting multiple views. Its internal geometry and line-grouping logic is already portable +(`Windows.Foundation.Rect`) if partial credit is wanted. + +### 4.6 Wave 6 — services and settings + +**6a — settings providers → plain Core.** `AutomationSettingsProvider.cs` (no changes) and +`AutomationProfile.cs` (widen `ApplySeed` to `ApplicationSettingsBase`). Add +`System.Configuration.ConfigurationManager` to `Text-Grab.Core.csproj`. Highest-confidence batch in +the wave, and it unblocks `TesseractHelper` (4b), `ContextMenuUtilities` and +`FileAssociationUtilities` (3a-deferred), and `FileUtilities` (5). + +**6b — speech.** `Services/WindowsSpeechEngine.cs` → Core.Windows. `Services/TtsService.cs` → +plain Core, after resolving its `private ITtsEngine _engine = new WindowsSpeechEngine();` field +initializer — the app should register the default engine at composition, same shape as +`SettingsAccess`. Add `TtsSpeakWordLimit`, `TtsVoiceName`, `TtsSpeakingRate`. + +**6c — `AudioTranscriptionUtilities.cs` → Core.Windows, wholesale.** 1115 lines, fully headless +(NAudio + Whisper.net, zero WPF, zero WinRT), with exactly **one** settings touchpoint: +`AudioTranscriptionModel`. Move `NAudio`, `Whisper.net`, `Whisper.net.Runtime` to Core.Windows. +(`IncludeTimecodesInTranscription` and `NotifyOnTranscriptionComplete` are consumed only in the +views, not in this file.) The cleanest single file in the whole reorganization — use it as the +anchor that proves Core.Windows can host NAudio/Whisper. + +**6d — `WebSearchUrlModel` split**, exactly `PatternItem`/`PatternItemCatalog`-shaped: pure record +→ Core, static accessors stay in the app. No interface changes needed. + +**6e — `HistoryService.cs`. Opus owns this; it is a second `OcrUtilities`.** A genuinely headless +JSON pipeline (`LoadHistoryAsync`, `LoadHistoryWithRecovery`, `WriteHistoryFiles`, the +`Normalize*` methods, `HistoryLanguageKindJsonConverter`) is interleaved with WPF menu building, +`GrabFrame`/`EditTextWindow` construction and a GDI+ `CachedBitmap`, sharing private state across +both halves. Blocked on `HistoryInfo`'s own `System.Windows.Rect PositionRect` — which B2 and 2d +now give a path to. + +### 4.7 Wave 7 — tests and closeout + +**7a — test migration.** To `Tests.Core`: `StringMethodTests`, `TextSearchUtilitiesTests`, +`RecognizerExecutorTests`, `PatternExecutorTests`, `CalculatorTests`, `UnitConversionTests`, +`ExtractedPatternTests`, `ColumnSplitUtilitiesTests`, `SpreadsheetUndoHistoryTests`, +`EditTextTableDocumentTests`, `GrabFrameTableEditStateTests`, `ThirdPartyNoticeUtilitiesTests`, +plus the pure halves of `ProtocolUtilitiesTests` and `MarkdownDocumentUtilitiesTests`. To +`Tests.Core.Windows`: `OcrTests` and the other headless-Windows suites. Delete +`Tests.Core/ScaffoldingSmokeTests.cs`. + +**7b — closeout.** Remove dead app-side shims; confirm the MSIX package still builds in Visual +Studio (the one thing the CLI gate cannot check); re-derive the never-move list one final time +against B2; update this document with the final layer map. + +### Never moves — verified `Views/`, `Controls/`, `Pages/`, `Styles/`, `Themes/`, `App.xaml.cs`, `AssemblyInfo.cs`, -`WPFExtensionMethods.cs`, `Properties/Settings.Designer.cs`, `Extensions/ControlExtensions`, -`DapploExtensions`, `KeyboardExtensions`, `ShapeExtensions`, `Utilities/ColorHelper`, -`CursorClipper`, `GrabFrameViewScaleUtilities`, `NotificationUtilities`, -`WindowSelectionUtilities`, `WindowResizer`, `WindowUtilities`, `HotKeyManager`, +`WPFExtensionMethods.cs`, `Properties/Settings.Designer.cs`, `TextGrabNotificationActivator.cs`. + +**Extensions:** `ControlExtensions`, `DapploExtensions`, `KeyboardExtensions`, `ShapeExtensions`. + +**Utilities:** `ColorHelper`, `CursorClipper`, `WindowResizer`, `WindowUtilities`, +`GrabFrameViewScaleUtilities`, `NotificationUtilities`, `WindowSelectionUtilities`, `HotKeyManager`, `AutomationDiagnostics`, `NotifyIconUtilities`, `OutputUtilities`, `ShareTargetUtilities`, -and the WPF-typed `UndoRedoOperations` (`AddWordBorder`, `RemoveWordBorder`, `ChangedImage`, -`Operation`) and Models (`FullscreenCaptureResult`, `PostGrabContext`, `ShortcutKeySet`, -`TemplateRegion`, `UiAutomationOptions`, `UiAutomationOverlayItem`, -`UiAutomationOverlaySnapshot`, `WindowSelectionCandidate`, `WordBorderInfo`, `ButtonInfo` — -the last uses `Wpf.Ui.Controls`). +`ImplementAppOptions`, `MagickHelpers`, `CameraCaptureUtilities`, `SettingsImportExportUtilities`, +`DiagnosticsUtilities`, `PostGrabActionManager`, `CustomBottomBarUtilities`, `ShortcutKeysUtilities`, +`UIAutomationUtilities` (see B4). + +**Models:** `ButtonInfo` (~90 static entries each assigning `Wpf.Ui.Controls.SymbolRegular` — +whole-class, not splittable), `ShortcutKeySet` (`System.Windows.Input.Key`), `PostGrabContext`, +`FullscreenCaptureResult`, `UiAutomationOptions`, `UiAutomationOverlayItem`, +`UiAutomationOverlaySnapshot`, `WindowSelectionCandidate`, `LookupItem`. + +**UndoRedoOperations:** all of them — `Operation`, `AddWordBorder`, `RemoveWordBorder`, +`ChangedImage`, `UndoRedo`, `ChangeWord`, `ResizeWordBorder`. Every one is typed on `WordBorder`, +`Canvas` or `ImageSource`. + +### Consolidated `ITextGrabSettings` additions -Several of the Models above become movable once B2 lands. Revisit them in Wave 7 rather than -guessing early. +Nine members across the whole plan, taking the interface from 10 to 19. Add each one only when its +batch runs. +| Property | Type | Needed by | Batch | +|---|---|---|---| +| `OverrideAiArchCheck` | `bool` | `WindowsAiUtilities` | 3 (deferred) | +| `UiAutomationEnabled` | `bool` | `LanguageService` | 4e | +| `WindowsAiDescriptionEnabled` | `bool` | `LanguageService` | 4e | +| `HdrCaptureCorrection` | `bool` | `ImageMethods` | 5c | +| `HdrBorderlessGranted` | `bool` | `HdrScreenCapture` | 5b | +| `AudioTranscriptionModel` | `string` | `AudioTranscriptionUtilities` | 6c | +| `TtsSpeakWordLimit` | `int` | `TtsService` | 6b | +| `TtsVoiceName` | `string` | `WindowsSpeechEngine` | 6b | +| `TtsSpeakingRate` | `double` | `WindowsSpeechEngine` | 6b | + +All nine already exist in `Settings.settings`, so each is a one-line interface addition with no +`.settings` edit. Declined: the three `UiAutomation*` traversal properties, per B4. + +**The `Load*`/`Save*` families are not candidates.** `LoadStoredRegexes`, `LoadBottomBarButtons`, +`LoadWebSearchUrls` and friends are `SettingsService` *methods*, not scalar properties. They do not +fit this interface's shape, and the façade pattern (`PatternItemCatalog`) handles them with no +interface change at all. ## 5. Sub-agent orchestration ### Roles @@ -326,22 +548,23 @@ simultaneously while you do Wave 0, so every mover starts with an accurate file ``` You are executing batch of the Text-Grab Core split. -Read D:\source\TheJoeFin\Text-Grab\docs\Core-Split-Plan.md first — sections 1, 2, and -your batch in section 4 are binding. Then read the two most recent move commits -(git show edefeaa, git show e677b54) to match the established style. +Read D:\source\TheJoeFin\Text-Grab\docs\Core-Split-Plan.md first — sections 1 (especially +invariant 8), 2, and your batch in section 4 are binding. Then read the two most recent +move commits (git show edefeaa, git show e677b54) to match the established style. Your file list is exactly: Target project: Procedure, per file: - 1. Read it. Confirm its actual dependencies — do not trust `using` lines alone. - `System.Drawing` primitives (RectangleF/PointF/SizeF) are portable and fine in Core; - `System.Drawing` GDI+ (Bitmap/Graphics/Icon) is Core.Windows only. + 1. Read it in full. Confirm which TYPES it uses — invariant 8 lists the four traps, and + the original wave lists were wrong about a dozen files for exactly these reasons. 2. If it moves cleanly: `git mv` it, keep the namespace, fix call sites the compiler flags. 3. If it needs a split: pure part moves, the coupled façade stays in the app under a new name. See PatternItem/PatternItemCatalog in e677b54 for the shape. 4. If it is blocked by something outside your list: LEAVE IT. Do not expand scope. + 5. If section 8 lists dead code in a file you are moving, re-verify it has no call sites + (beware target-typed `new()`), then delete it as part of the move. After each file, run: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 Never run `dotnet build Text-Grab.sln` — the wapproj fails under the dotnet CLI by design. @@ -380,6 +603,30 @@ Do not fire-and-forget the whole chain. Run **one wave at a time**, and between prior six commits were all human-reviewed; that ratio should hold — a mover that silently "fixes" a call site incorrectly compiles fine and breaks at runtime. +### What the reconnaissance pass actually bought + +Worth recording, because it justifies doing this again before Waves 5 and 6 execute. Five +read-only agents ran in parallel against one working tree — safe because none of them could +write. Between them they: + +- found `OSInterop.cs` (1292 lines) blocked by a single dead line; +- found the `AppUtilities.IsPackaged()` shared blocker that no single wave owned, which is now + Wave 1; +- **disproved four entries** on the original wave lists and **rescued two** from the never-move + list; +- settled the `System.Configuration`-on-net10.0 question by building and running a probe rather + than reasoning about it; +- surfaced the `FrameworkReference` loophole in invariant 2 (§B4). + +The cost was five agents reading ~60 files. The alternative was movers discovering each of these +mid-batch, with a half-applied commit in the tree. + +**Verify before acting on a report.** Every load-bearing claim above was independently checked +before it entered this document, and one was wrong in a way that mattered: an agent reported +`GrabFrame` constructing a `ResultTable`, and a naive `grep "new ResultTable"` appeared to refute +it — the call is target-typed `new()`. Trusting the grep over the agent would have deleted a live +constructor. + ## 6. Risk register | Risk | Mitigation | @@ -390,21 +637,49 @@ prior six commits were all human-reviewed; that ratio should hold — a mover th | `RuntimeIdentifiers` dropped from a library csproj | Causes NETSDK1047 in the wapproj restore only — invisible to the CLI gate. Grep the csprojs at wave boundaries. | | Settings interface sprawls to all 104 properties | Add properties only when a move demands one; if a batch needs more than ~5 new ones, that file probably wants a façade split instead. | | `EditTextWindow.xaml.cs` / `GrabFrame.xaml.cs` churn | They are touched by most batches. Serial movers make this safe; parallel ones would not. | +| A mover classifies by `using` lines and moves a WPF-bound file | Invariant 8. `TierBoundaryTests` catches it at the assembly level if it slips through. | +| A batch stalls because a one-line leaf it needs is still app-side | Wave 1 exists precisely for this. Do not start Waves 3–6 before it lands. | +| Deleting "dead" code that is actually reachable | §8 rows were each verified by full-repo grep. Re-verify before deleting; target-typed `new()` and same-named members on other types defeat a naive grep — that is how `GrabFrame`'s `ResultTable` construction was nearly missed. | ## 7. Deferred ledger -Files attempted and left behind, with the specific blocker. **Movers append here.** +Files with a real blocker, and the specific thing that clears it. **Movers append here.** +Every row below was verified by reading the file, not inferred. -| File | Blocker | Unblocked by | +| File | Blocker (specific) | Unblocked by | |---|---|---| -| `Utilities/OcrUtilities.cs` | 1061 lines mixing headless orchestration with WPF adapters; hidden WPF dep in `LoadBitmapFromFile`; class-level `AppUtilities.TextGrabSettings` field | Batch 4a (Opus) | -| `Utilities/TesseractHelper.cs` (class) | `GetTesseractPath()` writes back to settings as a side effect | B1 + lifting the write out | -| `Services/LanguageService.cs` | settings + WPF coupled; drags in several untouched types | B1, batch 4c | -| `Utilities/WindowsAiUtilities.cs` | settings + WPF coupled; drags in several untouched types | B1, batch 3c | -| `Utilities/BarcodeUtilities.cs` | `OcrOutput.CleanOutput()` reads settings directly | B1, batch 4b | -| HDR / WGC capture (`Utilities/Hdr/*`) | separate area, mostly clean already | batch 5b | - -## 8. Definition of done +| `Utilities/OcrUtilities.cs` | `LoadBitmapFromFile` (887–899) builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite | 4c, and defer that one method | +| `Utilities/TesseractHelper.cs` | `TempImagePath()` calls `AutomationProfile.GetTemporaryDirectory()`. The settings write-back is **not** a blocker — `ITextGrabSettings.Save()` already covers it | 6a | +| `Services/LanguageService.cs` | `System.Windows.Input.InputLanguageManager` (line 376), no portable substitute; reachable in production, not vestigial | Opus split (4e) | +| `Utilities/WindowsAiUtilities.cs` | `AutomationProfile.GetTemporaryFilePath()` (109, 189); `SoftwareBitmapExtensions` unmoved; reads `Settings.Default.OverrideAiArchCheck` directly (61) | 6a + 5a + one interface add | +| `Utilities/ContextMenuUtilities.cs` | `AutomationProfile.Current`, `FileUtilities.GetExePath()`; `IoUtilities` mixes pure extension lists with WinForms/Wpf.Ui `MessageBox` calls | 1a (IoUtilities split), 5, 6a | +| `Utilities/FileAssociationUtilities.cs` | `FileUtilities.GetExePath()` | Wave 5 | +| `Utilities/FileUtilities.cs` | `AutomationProfile.Current` in 6 methods | 6a | +| `Utilities/GrabFrameFileUtilities.cs` | Public signature bound to `HistoryInfo`, which needs B2 (`Rect PositionRect`), 2a (five enums) and 4e. A façade was considered and rejected as larger than the file it wraps | 2d + 4e, or `HistoryInfo` moving | +| `Services/SettingsService.cs` | Clones `ButtonInfo` and `ShortcutKeySet` field-by-field (both never-move); `Windows.Storage.ApplicationDataContainer` caps it at Core.Windows regardless | needs a `ButtonInfo` redesign — likely never | +| `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar | 2d, then a split | +| `Utilities/GrabTemplateExecutor.cs` | Template models not in Core; `LoadStoredRegexes()` needs a non-scalar seam; OCR half blocked on 4c | 2d + 4c + a façade | +| `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | +| `Services/HistoryService.cs` | Headless JSON pipeline interleaved with WPF menu building and `GrabFrame`/`EditTextWindow` construction, sharing private state | Opus split (6e) | + +## 8. Verified dead code — free, zero-risk prep + +Each of these was confirmed to have **zero call sites** across the repo. Deleting them is safe +and independent of any wave; two of them unblock real moves. + +| Dead code | Why it matters | +|---|---| +| `OSInterop.GetAsyncKeyState(System.Windows.Forms.Keys)` (line 125) | The **only** `System.Windows.Forms` reference in all 1292 lines. Deleting it moves the whole file. | +| `Models/DragDataObject.BitmapSourceToBitmap` (line 77) | The file's only WPF touchpoint, and a duplicate of `ImageMethods.BitmapSourceToBitmap`. Deleting it moves the file. | +| `Models/ResultTable`: `OcrResult` property, `ParseOcrResultWordsIntoRects()`, the `ResultTable(ref List, DpiScale)` ctor, `CalculateResultRows`, `MergeTheseRowIDs` | Leftovers from a superseded grid-line algorithm. The `OcrResult` property is why `ResultTable` looked WinRT-coupled; the live path already uses `IOcrLinesWords`. | +| `Utilities/OcrUtilities.GetBoundingRect(this OcrLine)` (line 983) | `edefeaa` left it saying "other app code may still use it". Nothing does. | +| `Extensions/ImageExtensions.ExifRotate` (line 12) | Unused. | + +Not dead, but a one-line dependency removal in the same spirit: +`ClipboardUtilities.cs:64` uses `System.Windows.Forms.DataFormats.Bitmap` — the identical string +constant to WPF's `System.Windows.DataFormats.Bitmap`, and the file's only WinForms use. + +## 9. Definition of done - `Text-Grab.Core` holds the text, pattern, table, calculation, and template logic, with no `System.Windows`, no `Windows.*`, no P/Invoke. From d251324a5a7de76f626b077c205cb0dda5e87a76 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:12:00 -0500 Subject: [PATCH 12/54] Move Singleton, StreamWrapper, NullAsyncResult and Json into Text-Grab.Core Wave 1a - shared leaves that were blocking Waves 3, 5 and 6 independently. All four verified dependency-free per invariant 8: - Singleton.cs and StreamWrapper.cs (WrappingStream) moved as-is; the latter constructs NullAsyncResult, which moved in the same commit to keep the pair compiling together. - Json.cs kept its Text_Grab.Helpers namespace unchanged even though it physically lived under Utilities/ - not "fixed" to match the folder. IoUtilities.cs needed a real split. The extension lists and the Is*File/Is*FileExtension predicates plus ListFilesFoldersInDirectory are pure string/List logic and moved to Text-Grab.Core as-is. GetEditorModeForPath and GetOpenContentKindForPath return EtwEditorMode and OpenContentKind, both still in the app's Enums.cs (that merge is Wave 2a, not this batch), so they can't move yet. GetContentFromPath and TryToOpenTextFile also stay app-side - they call OcrUtilities and show Wpf.Ui/WinForms MessageBoxes. All four impure members moved into a new Text-Grab/Utilities/FileOpenUtilities.cs facade (the IoUtilities name had to move with the pure half to avoid a duplicate-type collision once the app references Core). Updated the four call sites this renamed: EditTextWindow.xaml.cs, App.xaml.cs, and Tests/FilesIoTests.cs (both enum-returning test cases). No files deferred from this batch's list. Co-Authored-By: Claude Opus 5 --- Tests/FilesIoTests.cs | 4 +- .../Models/NullAsyncResult.cs | 0 .../Utilities/IoUtilities.cs | 87 +---------------- .../Utilities/Json.cs | 0 .../Utilities/Singleton.cs | 0 .../Utilities/StreamWrapper.cs | 0 Text-Grab/App.xaml.cs | 2 +- Text-Grab/Utilities/FileOpenUtilities.cs | 93 +++++++++++++++++++ Text-Grab/Views/EditTextWindow.xaml.cs | 4 +- 9 files changed, 99 insertions(+), 91 deletions(-) rename {Text-Grab => Text-Grab.Core}/Models/NullAsyncResult.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/IoUtilities.cs (52%) rename {Text-Grab => Text-Grab.Core}/Utilities/Json.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/Singleton.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/StreamWrapper.cs (100%) create mode 100644 Text-Grab/Utilities/FileOpenUtilities.cs diff --git a/Tests/FilesIoTests.cs b/Tests/FilesIoTests.cs index 6560b7ca..2e3d7ba5 100644 --- a/Tests/FilesIoTests.cs +++ b/Tests/FilesIoTests.cs @@ -107,7 +107,7 @@ public async Task ReadNotExistingImageFileEmpty(FileStorageKind storageKind) [InlineData(@"C:\Temp\data.json", EtwEditorMode.Text)] public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode expectedMode) { - Assert.Equal(expectedMode, IoUtilities.GetEditorModeForPath(path)); + Assert.Equal(expectedMode, FileOpenUtilities.GetEditorModeForPath(path)); } [Theory] @@ -116,7 +116,7 @@ public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode ex [InlineData(@"C:\Temp\notes.txt", OpenContentKind.TextFile)] public void GetOpenContentKindForPath_ClassifiesVisualDocumentsAndText(string path, OpenContentKind expectedKind) { - Assert.Equal(expectedKind, IoUtilities.GetOpenContentKindForPath(path)); + Assert.Equal(expectedKind, FileOpenUtilities.GetOpenContentKindForPath(path)); } [Theory] diff --git a/Text-Grab/Models/NullAsyncResult.cs b/Text-Grab.Core/Models/NullAsyncResult.cs similarity index 100% rename from Text-Grab/Models/NullAsyncResult.cs rename to Text-Grab.Core/Models/NullAsyncResult.cs diff --git a/Text-Grab/Utilities/IoUtilities.cs b/Text-Grab.Core/Utilities/IoUtilities.cs similarity index 52% rename from Text-Grab/Utilities/IoUtilities.cs rename to Text-Grab.Core/Utilities/IoUtilities.cs index 698bf16b..4052f179 100644 --- a/Text-Grab/Utilities/IoUtilities.cs +++ b/Text-Grab.Core/Utilities/IoUtilities.cs @@ -1,10 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; -using System.Threading.Tasks; -using Text_Grab.Interfaces; -using Text_Grab.Models; namespace Text_Grab.Utilities; @@ -76,88 +73,6 @@ public static bool IsSpreadsheetFileExtension(string extension) return SpreadsheetExtensions.Contains(extension.ToLowerInvariant()); } - public static EtwEditorMode GetEditorModeForPath(string? path) - { - string extension = Path.GetExtension(path ?? string.Empty); - - if (IsSpreadsheetFileExtension(extension)) - return EtwEditorMode.Spreadsheet; - - if (IsMarkdownFileExtension(extension)) - return EtwEditorMode.Markdown; - - return EtwEditorMode.Text; - } - - public static OpenContentKind GetOpenContentKindForPath(string? path) - { - string extension = Path.GetExtension(path ?? string.Empty); - - if (IsPdfFileExtension(extension)) - return OpenContentKind.PdfDocument; - - if (IsImageFileExtension(extension)) - return OpenContentKind.Image; - - return OpenContentKind.TextFile; - } - - public static async Task<(string TextContent, OpenContentKind SourceKindOfContent)> GetContentFromPath(string pathOfFileToOpen, bool isMultipleFiles = false, ILanguage? language = null) - { - StringBuilder stringBuilder = new(); - OpenContentKind openContentKind = GetOpenContentKindForPath(pathOfFileToOpen); - - if (isMultipleFiles) - stringBuilder.AppendLine(pathOfFileToOpen); - - if (openContentKind is OpenContentKind.Image or OpenContentKind.PdfDocument) - { - try - { - stringBuilder.Append(await OcrUtilities.OcrAbsoluteFilePathAsync(pathOfFileToOpen, language)); - } - catch (Exception) - { - await new Wpf.Ui.Controls.MessageBox - { - Title = "Error", - Content = $"Failed to read {pathOfFileToOpen}", - CloseButtonText = "OK" - }.ShowDialogAsync(); - } - } - else - { - // Continue with along trying to open a text file. - openContentKind = OpenContentKind.TextFile; - await TryToOpenTextFile(pathOfFileToOpen, isMultipleFiles, stringBuilder); - } - - if (isMultipleFiles) - { - stringBuilder.Append(Environment.NewLine); - stringBuilder.Append(Environment.NewLine); - } - - return (stringBuilder.ToString(), openContentKind); - } - - public static async Task TryToOpenTextFile(string pathOfFileToOpen, bool isMultipleFiles, StringBuilder stringBuilder) - { - try - { - using StreamReader sr = File.OpenText(pathOfFileToOpen); - - string s = await sr.ReadToEndAsync(); - - stringBuilder.Append(s); - } - catch (System.Exception ex) - { - System.Windows.Forms.MessageBox.Show($"Failed to open file. {ex.Message}"); - } - } - public static string ListFilesFoldersInDirectory(string chosenFolderPath) { IEnumerable files = Directory.EnumerateFiles(chosenFolderPath); diff --git a/Text-Grab/Utilities/Json.cs b/Text-Grab.Core/Utilities/Json.cs similarity index 100% rename from Text-Grab/Utilities/Json.cs rename to Text-Grab.Core/Utilities/Json.cs diff --git a/Text-Grab/Utilities/Singleton.cs b/Text-Grab.Core/Utilities/Singleton.cs similarity index 100% rename from Text-Grab/Utilities/Singleton.cs rename to Text-Grab.Core/Utilities/Singleton.cs diff --git a/Text-Grab/Utilities/StreamWrapper.cs b/Text-Grab.Core/Utilities/StreamWrapper.cs similarity index 100% rename from Text-Grab/Utilities/StreamWrapper.cs rename to Text-Grab.Core/Utilities/StreamWrapper.cs diff --git a/Text-Grab/App.xaml.cs b/Text-Grab/App.xaml.cs index 2c50f2d1..a27d2dd6 100644 --- a/Text-Grab/App.xaml.cs +++ b/Text-Grab/App.xaml.cs @@ -522,7 +522,7 @@ public static async Task TryToOpenFilePathAsync(string possiblePath, bool if (isQuiet) { - (string pathContent, _) = await IoUtilities.GetContentFromPath(possiblePath); + (string pathContent, _) = await FileOpenUtilities.GetContentFromPath(possiblePath); OutputUtilities.HandleTextFromOcr( pathContent, false, diff --git a/Text-Grab/Utilities/FileOpenUtilities.cs b/Text-Grab/Utilities/FileOpenUtilities.cs new file mode 100644 index 00000000..14b8076f --- /dev/null +++ b/Text-Grab/Utilities/FileOpenUtilities.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Text_Grab.Interfaces; +using Text_Grab.Models; + +namespace Text_Grab.Utilities; + +public class FileOpenUtilities +{ + public static EtwEditorMode GetEditorModeForPath(string? path) + { + string extension = Path.GetExtension(path ?? string.Empty); + + if (IoUtilities.IsSpreadsheetFileExtension(extension)) + return EtwEditorMode.Spreadsheet; + + if (IoUtilities.IsMarkdownFileExtension(extension)) + return EtwEditorMode.Markdown; + + return EtwEditorMode.Text; + } + + public static OpenContentKind GetOpenContentKindForPath(string? path) + { + string extension = Path.GetExtension(path ?? string.Empty); + + if (IoUtilities.IsPdfFileExtension(extension)) + return OpenContentKind.PdfDocument; + + if (IoUtilities.IsImageFileExtension(extension)) + return OpenContentKind.Image; + + return OpenContentKind.TextFile; + } + + public static async Task<(string TextContent, OpenContentKind SourceKindOfContent)> GetContentFromPath(string pathOfFileToOpen, bool isMultipleFiles = false, ILanguage? language = null) + { + StringBuilder stringBuilder = new(); + OpenContentKind openContentKind = GetOpenContentKindForPath(pathOfFileToOpen); + + if (isMultipleFiles) + stringBuilder.AppendLine(pathOfFileToOpen); + + if (openContentKind is OpenContentKind.Image or OpenContentKind.PdfDocument) + { + try + { + stringBuilder.Append(await OcrUtilities.OcrAbsoluteFilePathAsync(pathOfFileToOpen, language)); + } + catch (Exception) + { + await new Wpf.Ui.Controls.MessageBox + { + Title = "Error", + Content = $"Failed to read {pathOfFileToOpen}", + CloseButtonText = "OK" + }.ShowDialogAsync(); + } + } + else + { + // Continue with along trying to open a text file. + openContentKind = OpenContentKind.TextFile; + await TryToOpenTextFile(pathOfFileToOpen, isMultipleFiles, stringBuilder); + } + + if (isMultipleFiles) + { + stringBuilder.Append(Environment.NewLine); + stringBuilder.Append(Environment.NewLine); + } + + return (stringBuilder.ToString(), openContentKind); + } + + public static async Task TryToOpenTextFile(string pathOfFileToOpen, bool isMultipleFiles, StringBuilder stringBuilder) + { + try + { + using StreamReader sr = File.OpenText(pathOfFileToOpen); + + string s = await sr.ReadToEndAsync(); + + stringBuilder.Append(s); + } + catch (System.Exception ex) + { + System.Windows.Forms.MessageBox.Show($"Failed to open file. {ex.Message}"); + } + } +} diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index 88db5ffc..dbce2a9d 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -2522,14 +2522,14 @@ internal async void OpenPath(string pathOfFileToOpen, bool isMultipleFiles = fal } ResetSpreadsheetUndoHistory(); - (string TextContent, OpenContentKind KindOpened) = await IoUtilities.GetContentFromPath(pathOfFileToOpen, isMultipleFiles, selectedILanguage); + (string TextContent, OpenContentKind KindOpened) = await FileOpenUtilities.GetContentFromPath(pathOfFileToOpen, isMultipleFiles, selectedILanguage); bool shouldTrackOpenedFile = KindOpened == OpenContentKind.TextFile && !isMultipleFiles; if (KindOpened == OpenContentKind.TextFile) { EtwEditorMode targetMode = isMultipleFiles ? EtwEditorMode.Text - : IoUtilities.GetEditorModeForPath(pathOfFileToOpen); + : FileOpenUtilities.GetEditorModeForPath(pathOfFileToOpen); if (IsLoaded) SetEditorMode(targetMode); From bf171f7fe60bb492f3ab4c6efff20486a2c072f0 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:15:46 -0500 Subject: [PATCH 13/54] Extract packaging identity and move StorageFileExtensions into Text-Grab.Core.Windows Wave 1b - the two remaining shared leaves blocking Waves 3 and 5. AppUtilities.IsPackaged()/GetAppVersion() needed only Windows.ApplicationModel. Package; they were unreachable from Core.Windows purely because they shared a class with TextGrabSettings/TextGrabSettingsService, which must stay in the app. Extracted both into new Text-Grab.Core.Windows/Utilities/PackageIdentity.cs (namespace Text_Grab.Utilities, unchanged), keeping the same try/catch and version-string logic verbatim. AppUtilities.IsPackaged()/GetAppVersion() are now one-line forwarders to PackageIdentity, so the ~30 existing call sites across the app (GeneralSettings, LanguageSettings, SettingsService, FileUtilities, WindowsAiUtilities, etc.) did not need to change. StorageFileExtensions.cs moved as-is via git mv - verified dependency-free (System, System.IO, Windows.Storage, Windows.Storage.Streams only, no Text-Grab types). Namespace Text_Grab.Extensions unchanged. No files deferred from this batch's list. Co-Authored-By: Claude Opus 5 --- .../Extensions/StorageFileExtensions.cs | 0 .../Utilities/PackageIdentity.cs | 37 +++++++++++++++++++ Text-Grab/Utilities/AppUtilities.cs | 27 +------------- 3 files changed, 39 insertions(+), 25 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Extensions/StorageFileExtensions.cs (100%) create mode 100644 Text-Grab.Core.Windows/Utilities/PackageIdentity.cs diff --git a/Text-Grab/Extensions/StorageFileExtensions.cs b/Text-Grab.Core.Windows/Extensions/StorageFileExtensions.cs similarity index 100% rename from Text-Grab/Extensions/StorageFileExtensions.cs rename to Text-Grab.Core.Windows/Extensions/StorageFileExtensions.cs diff --git a/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs b/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs new file mode 100644 index 00000000..78753cc1 --- /dev/null +++ b/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs @@ -0,0 +1,37 @@ +using Windows.ApplicationModel; + +namespace Text_Grab.Utilities; + +/// +/// Packaging identity checks that need only . Split out of +/// Text-Grab/Utilities/AppUtilities.cs so this piece can live in Core.Windows while +/// TextGrabSettings/TextGrabSettingsService stay in the app; AppUtilities.IsPackaged() +/// and GetAppVersion() forward here. +/// +public static class PackageIdentity +{ + public static bool IsPackaged() + { + try + { + // If we have a package ID then we are running in a packaged context + PackageId dummy = Package.Current.Id; + return true; + } + catch + { + return false; + } + } + + public static string GetAppVersion() + { + if (IsPackaged()) + { + PackageVersion version = Package.Current.Id.Version; + return $"{version.Major}.{version.Minor}.{version.Build}" ?? "unknown error reading package version"; + } + + return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown error reading assembly version"; + } +} diff --git a/Text-Grab/Utilities/AppUtilities.cs b/Text-Grab/Utilities/AppUtilities.cs index 2a64ce55..80d7bcf1 100644 --- a/Text-Grab/Utilities/AppUtilities.cs +++ b/Text-Grab/Utilities/AppUtilities.cs @@ -1,23 +1,10 @@ using Text_Grab.Properties; using Text_Grab.Services; -using Windows.ApplicationModel; namespace Text_Grab.Utilities; internal class AppUtilities { - internal static bool IsPackaged() - { - try - { - // If we have a package ID then we are running in a packaged context - PackageId dummy = Package.Current.Id; - return true; - } - catch - { - return false; - } - } + internal static bool IsPackaged() => PackageIdentity.IsPackaged(); internal static SettingsService TextGrabSettingsService => Singleton.Instance; @@ -33,15 +20,5 @@ internal static bool ShouldCorrectToLatin() && settings.CorrectToLatin && LanguageUtilities.IsCurrentLanguageLatinBased(); - internal static string GetAppVersion() - { - if (IsPackaged()) - { - PackageVersion version = Package.Current.Id.Version; - return $"{version.Major}.{version.Minor}.{version.Build}" ?? "unknown error reading package version"; - } - - - return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown error reading assembly version"; - } + internal static string GetAppVersion() => PackageIdentity.GetAppVersion(); } From 9c4d2e5027c86d6d6498cd4ad026d3fa6fe6972f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:17:01 -0500 Subject: [PATCH 14/54] Read the app version from the entry assembly, not the executing one GetAppVersion moved into Text-Grab.Core.Windows in the previous commit, which silently changed what GetExecutingAssembly() resolves to. In AppUtilities it was Text-Grab.exe and its 4.15.0; from Core.Windows it is a library with no version property, so the unpackaged path reported 1.0.0.0 to the settings page and to the diagnostics payload. The entry assembly is the app either way. Co-Authored-By: Claude Opus 5 --- Text-Grab.Core.Windows/Utilities/PackageIdentity.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs b/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs index 78753cc1..938b281c 100644 --- a/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs +++ b/Text-Grab.Core.Windows/Utilities/PackageIdentity.cs @@ -32,6 +32,13 @@ public static string GetAppVersion() return $"{version.Major}.{version.Minor}.{version.Build}" ?? "unknown error reading package version"; } - return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown error reading assembly version"; + // Deliberately the ENTRY assembly, not the executing one. This method used to live in + // Text-Grab/Utilities/AppUtilities.cs, where "executing" meant Text-Grab.exe and its + // property. From here, "executing" would mean Text-Grab.Core.Windows, which + // carries no version and would report 1.0.0.0 to the settings page and diagnostics. + System.Reflection.Assembly versionSource = + System.Reflection.Assembly.GetEntryAssembly() ?? System.Reflection.Assembly.GetExecutingAssembly(); + + return versionSource.GetName().Version?.ToString() ?? "unknown error reading assembly version"; } } From ce24f0bb6349d2e7c9c377cbb3866f8da5b1118e Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:20:53 -0500 Subject: [PATCH 15/54] Merge Enums.cs into Text-Grab.Core and move GetOpenContentKindForPath Batch 2a. Verified all 17 enums in Text-Grab/Enums.cs are plain int/short-backed with no attributes, and confirmed zero name collisions with Core's existing CurrentCase/SpotInLine before merging them into Text-Grab.Core/Enums.cs. Deleted the now-empty app Enums.cs. Revisited FileOpenUtilities.cs as directed. GetOpenContentKindForPath returns OpenContentKind, which just landed in Core, so it moved to Text-Grab.Core/Utilities/IoUtilities.cs alongside the extension predicates it already calls; FileOpenUtilities.GetContentFromPath and Tests/FilesIoTests.cs were updated to call it there. GetEditorModeForPath did NOT move, despite the plan's premise that both methods were blocked only by Enums.cs. Verified by reading EditTextTableDocument.cs and checking `git log`: EtwEditorMode has never lived in Enums.cs - it was declared directly in Text-Grab/Models/EditTextTableDocument.cs since that file was created. GetEditorModeForPath stays in FileOpenUtilities.cs until batch 2b moves EditTextTableDocument.cs (and EtwEditorMode with it). Gate: Tests.Core 14/14, Tests.Core.Windows 3/3, Tests 1493/7 skipped/0 failed - matches baseline. Co-Authored-By: Claude Opus 5 --- Tests/FilesIoTests.cs | 2 +- Text-Grab.Core/Enums.cs | 131 ++++++++++++++++++++++ Text-Grab.Core/Utilities/IoUtilities.cs | 13 +++ Text-Grab/Enums.cs | 132 ----------------------- Text-Grab/Utilities/FileOpenUtilities.cs | 15 +-- 5 files changed, 146 insertions(+), 147 deletions(-) delete mode 100644 Text-Grab/Enums.cs diff --git a/Tests/FilesIoTests.cs b/Tests/FilesIoTests.cs index 2e3d7ba5..622f1662 100644 --- a/Tests/FilesIoTests.cs +++ b/Tests/FilesIoTests.cs @@ -116,7 +116,7 @@ public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode ex [InlineData(@"C:\Temp\notes.txt", OpenContentKind.TextFile)] public void GetOpenContentKindForPath_ClassifiesVisualDocumentsAndText(string path, OpenContentKind expectedKind) { - Assert.Equal(expectedKind, FileOpenUtilities.GetOpenContentKindForPath(path)); + Assert.Equal(expectedKind, IoUtilities.GetOpenContentKindForPath(path)); } [Theory] diff --git a/Text-Grab.Core/Enums.cs b/Text-Grab.Core/Enums.cs index 15cb5b30..490b2c5e 100644 --- a/Text-Grab.Core/Enums.cs +++ b/Text-Grab.Core/Enums.cs @@ -13,3 +13,134 @@ public enum SpotInLine Beginning = 0, End = 1, } + +public enum AddRemove +{ + Add = 0, + Remove = 1, +} + +public enum AppTheme +{ + System = 0, + Dark = 1, + Light = 2 +} + +public enum TrayIconStyle +{ + Color = 0, + Monochrome = 1, +} + +public enum FileStorageKind +{ + Absolute = 0, + WithExe = 1, + WithHistory = 2, +} + +public enum OpenContentKind +{ + Image = 0, + TextFile = 1, + Directory = 2, + PdfDocument = 3, +} + +public enum OcrEngineKind +{ + Windows = 0, + Tesseract = 1, +} + +public enum OcrOutputKind +{ + None = 0, + Line = 1, + Paragraph = 2, + Barcode = 3, +} + +public enum Side +{ + None = 0, + Left = 1, + Right = 2, + Top = 3, + Bottom = 4 +} + +public enum TextGrabMode +{ + Fullscreen = 0, + GrabFrame = 1, + EditText = 2, + QuickLookup = 3 +} +public enum VirtualKeyCodes : short +{ + LeftButton = 0x01, + RightButton = 0x02, + MiddleButton = 0x04 +} + +public enum ScrollBehavior +{ + None = 0, + Resize = 1, + Zoom = 2, + ZoomWhenFrozen = 3, +} + +public enum GrabFrameBorderStyle +{ + // Follow the app light/dark theme (current behavior). + Theme = 0, + // Two-tone white+black border; one tone always contrasts with any background. + HighContrast = 1, + // A fixed user-picked color. + Color = 2, +} + +public enum SpellCheckMode +{ + // Enable spell check unless the text looks like it would choke the checker + // (very long documents or several long unspaced tokens). + Auto = 0, + // Always show spell check, regardless of content. + AlwaysOn = 1, + // Never show spell check. + Off = 2, +} + +public enum LanguageKind +{ + Global = 0, + Tesseract = 1, + WindowsAi = 2, + UiAutomation = 3, + WindowsAiDescription = 4, +} + +public enum UiAutomationTraversalMode +{ + Fast = 0, + Balanced = 1, + Thorough = 2, +} + +public enum FsgDefaultMode +{ + Default = 0, + SingleLine = 1, + Table = 2, +} + +public enum FsgSelectionStyle +{ + Region = 0, + Window = 1, + Freeform = 2, + AdjustAfter = 3, +} diff --git a/Text-Grab.Core/Utilities/IoUtilities.cs b/Text-Grab.Core/Utilities/IoUtilities.cs index 4052f179..aa2bab6d 100644 --- a/Text-Grab.Core/Utilities/IoUtilities.cs +++ b/Text-Grab.Core/Utilities/IoUtilities.cs @@ -73,6 +73,19 @@ public static bool IsSpreadsheetFileExtension(string extension) return SpreadsheetExtensions.Contains(extension.ToLowerInvariant()); } + public static OpenContentKind GetOpenContentKindForPath(string? path) + { + string extension = Path.GetExtension(path ?? string.Empty); + + if (IsPdfFileExtension(extension)) + return OpenContentKind.PdfDocument; + + if (IsImageFileExtension(extension)) + return OpenContentKind.Image; + + return OpenContentKind.TextFile; + } + public static string ListFilesFoldersInDirectory(string chosenFolderPath) { IEnumerable files = Directory.EnumerateFiles(chosenFolderPath); diff --git a/Text-Grab/Enums.cs b/Text-Grab/Enums.cs deleted file mode 100644 index 264ff58e..00000000 --- a/Text-Grab/Enums.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace Text_Grab; - -public enum AddRemove -{ - Add = 0, - Remove = 1, -} - -public enum AppTheme -{ - System = 0, - Dark = 1, - Light = 2 -} - -public enum TrayIconStyle -{ - Color = 0, - Monochrome = 1, -} - -public enum FileStorageKind -{ - Absolute = 0, - WithExe = 1, - WithHistory = 2, -} - -public enum OpenContentKind -{ - Image = 0, - TextFile = 1, - Directory = 2, - PdfDocument = 3, -} - -public enum OcrEngineKind -{ - Windows = 0, - Tesseract = 1, -} - -public enum OcrOutputKind -{ - None = 0, - Line = 1, - Paragraph = 2, - Barcode = 3, -} - -public enum Side -{ - None = 0, - Left = 1, - Right = 2, - Top = 3, - Bottom = 4 -} - -public enum TextGrabMode -{ - Fullscreen = 0, - GrabFrame = 1, - EditText = 2, - QuickLookup = 3 -} -public enum VirtualKeyCodes : short -{ - LeftButton = 0x01, - RightButton = 0x02, - MiddleButton = 0x04 -} - -public enum ScrollBehavior -{ - None = 0, - Resize = 1, - Zoom = 2, - ZoomWhenFrozen = 3, -} - -public enum GrabFrameBorderStyle -{ - // Follow the app light/dark theme (current behavior). - Theme = 0, - // Two-tone white+black border; one tone always contrasts with any background. - HighContrast = 1, - // A fixed user-picked color. - Color = 2, -} - -public enum SpellCheckMode -{ - // Enable spell check unless the text looks like it would choke the checker - // (very long documents or several long unspaced tokens). - Auto = 0, - // Always show spell check, regardless of content. - AlwaysOn = 1, - // Never show spell check. - Off = 2, -} - -public enum LanguageKind -{ - Global = 0, - Tesseract = 1, - WindowsAi = 2, - UiAutomation = 3, - WindowsAiDescription = 4, -} - -public enum UiAutomationTraversalMode -{ - Fast = 0, - Balanced = 1, - Thorough = 2, -} - -public enum FsgDefaultMode -{ - Default = 0, - SingleLine = 1, - Table = 2, -} - -public enum FsgSelectionStyle -{ - Region = 0, - Window = 1, - Freeform = 2, - AdjustAfter = 3, -} diff --git a/Text-Grab/Utilities/FileOpenUtilities.cs b/Text-Grab/Utilities/FileOpenUtilities.cs index 14b8076f..2ba06652 100644 --- a/Text-Grab/Utilities/FileOpenUtilities.cs +++ b/Text-Grab/Utilities/FileOpenUtilities.cs @@ -22,23 +22,10 @@ public static EtwEditorMode GetEditorModeForPath(string? path) return EtwEditorMode.Text; } - public static OpenContentKind GetOpenContentKindForPath(string? path) - { - string extension = Path.GetExtension(path ?? string.Empty); - - if (IoUtilities.IsPdfFileExtension(extension)) - return OpenContentKind.PdfDocument; - - if (IoUtilities.IsImageFileExtension(extension)) - return OpenContentKind.Image; - - return OpenContentKind.TextFile; - } - public static async Task<(string TextContent, OpenContentKind SourceKindOfContent)> GetContentFromPath(string pathOfFileToOpen, bool isMultipleFiles = false, ILanguage? language = null) { StringBuilder stringBuilder = new(); - OpenContentKind openContentKind = GetOpenContentKindForPath(pathOfFileToOpen); + OpenContentKind openContentKind = IoUtilities.GetOpenContentKindForPath(pathOfFileToOpen); if (isMultipleFiles) stringBuilder.AppendLine(pathOfFileToOpen); From 35f9b14604d92597d8e3a1ae4588437fd3155482 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:23:52 -0500 Subject: [PATCH 16/54] Move ten Models files into Text-Grab.Core and finish the FileOpenUtilities split Batch 2b. All ten files read in full and verified clean per invariant 8: AsyncOcrFileResult, EditTextTableDocument (moved first - FindResult calls its static GetSpreadsheetColumnLabel), ExtractedPattern (uses StringMethods.ExtractSimplePattern, already in Core), FindResult, GrabFrameTableEditState, GrabFrameWordGroupingMode, SpreadsheetUndoHistory (internal - already covered by Core's existing InternalsVisibleTo("Tests") and ("Text-Grab")), TemplatePatternMatch, TemplateRecognizerMatch (uses RecognizerOutputKind, already in Core), ThirdPartyPackageInfo. No call sites needed fixing - everything resolved through the existing Text_Grab.Models namespace. This unblocks the other half of the FileOpenUtilities revisit deferred in the previous commit: EtwEditorMode moved to Core along with EditTextTableDocument.cs, so GetEditorModeForPath now moves too, joining GetOpenContentKindForPath in Text-Grab.Core/Utilities/IoUtilities.cs. FileOpenUtilities.cs is left with only GetContentFromPath and TryToOpenTextFile, which stay app-side (OcrUtilities call, Wpf.Ui/WinForms MessageBoxes). Updated call sites: EditTextWindow.xaml.cs and Tests/FilesIoTests.cs. Gate: Tests.Core 14/14, Tests.Core.Windows 3/3, Tests 1493/7 skipped/0 failed - matches baseline. Co-Authored-By: Claude Opus 5 --- Tests/FilesIoTests.cs | 2 +- .../Models/AsyncOcrFileResult.cs | 0 .../Models/EditTextTableDocument.cs | 0 .../Models/ExtractedPattern.cs | 0 {Text-Grab => Text-Grab.Core}/Models/FindResult.cs | 0 .../Models/GrabFrameTableEditState.cs | 0 .../Models/GrabFrameWordGroupingMode.cs | 0 .../Models/SpreadsheetUndoHistory.cs | 0 .../Models/TemplatePatternMatch.cs | 0 .../Models/TemplateRecognizerMatch.cs | 0 .../Models/ThirdPartyPackageInfo.cs | 0 Text-Grab.Core/Utilities/IoUtilities.cs | 14 ++++++++++++++ Text-Grab/Utilities/FileOpenUtilities.cs | 14 -------------- Text-Grab/Views/EditTextWindow.xaml.cs | 2 +- 14 files changed, 16 insertions(+), 16 deletions(-) rename {Text-Grab => Text-Grab.Core}/Models/AsyncOcrFileResult.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/EditTextTableDocument.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/ExtractedPattern.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/FindResult.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/GrabFrameTableEditState.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/GrabFrameWordGroupingMode.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/SpreadsheetUndoHistory.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/TemplatePatternMatch.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/TemplateRecognizerMatch.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/ThirdPartyPackageInfo.cs (100%) diff --git a/Tests/FilesIoTests.cs b/Tests/FilesIoTests.cs index 622f1662..6560b7ca 100644 --- a/Tests/FilesIoTests.cs +++ b/Tests/FilesIoTests.cs @@ -107,7 +107,7 @@ public async Task ReadNotExistingImageFileEmpty(FileStorageKind storageKind) [InlineData(@"C:\Temp\data.json", EtwEditorMode.Text)] public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode expectedMode) { - Assert.Equal(expectedMode, FileOpenUtilities.GetEditorModeForPath(path)); + Assert.Equal(expectedMode, IoUtilities.GetEditorModeForPath(path)); } [Theory] diff --git a/Text-Grab/Models/AsyncOcrFileResult.cs b/Text-Grab.Core/Models/AsyncOcrFileResult.cs similarity index 100% rename from Text-Grab/Models/AsyncOcrFileResult.cs rename to Text-Grab.Core/Models/AsyncOcrFileResult.cs diff --git a/Text-Grab/Models/EditTextTableDocument.cs b/Text-Grab.Core/Models/EditTextTableDocument.cs similarity index 100% rename from Text-Grab/Models/EditTextTableDocument.cs rename to Text-Grab.Core/Models/EditTextTableDocument.cs diff --git a/Text-Grab/Models/ExtractedPattern.cs b/Text-Grab.Core/Models/ExtractedPattern.cs similarity index 100% rename from Text-Grab/Models/ExtractedPattern.cs rename to Text-Grab.Core/Models/ExtractedPattern.cs diff --git a/Text-Grab/Models/FindResult.cs b/Text-Grab.Core/Models/FindResult.cs similarity index 100% rename from Text-Grab/Models/FindResult.cs rename to Text-Grab.Core/Models/FindResult.cs diff --git a/Text-Grab/Models/GrabFrameTableEditState.cs b/Text-Grab.Core/Models/GrabFrameTableEditState.cs similarity index 100% rename from Text-Grab/Models/GrabFrameTableEditState.cs rename to Text-Grab.Core/Models/GrabFrameTableEditState.cs diff --git a/Text-Grab/Models/GrabFrameWordGroupingMode.cs b/Text-Grab.Core/Models/GrabFrameWordGroupingMode.cs similarity index 100% rename from Text-Grab/Models/GrabFrameWordGroupingMode.cs rename to Text-Grab.Core/Models/GrabFrameWordGroupingMode.cs diff --git a/Text-Grab/Models/SpreadsheetUndoHistory.cs b/Text-Grab.Core/Models/SpreadsheetUndoHistory.cs similarity index 100% rename from Text-Grab/Models/SpreadsheetUndoHistory.cs rename to Text-Grab.Core/Models/SpreadsheetUndoHistory.cs diff --git a/Text-Grab/Models/TemplatePatternMatch.cs b/Text-Grab.Core/Models/TemplatePatternMatch.cs similarity index 100% rename from Text-Grab/Models/TemplatePatternMatch.cs rename to Text-Grab.Core/Models/TemplatePatternMatch.cs diff --git a/Text-Grab/Models/TemplateRecognizerMatch.cs b/Text-Grab.Core/Models/TemplateRecognizerMatch.cs similarity index 100% rename from Text-Grab/Models/TemplateRecognizerMatch.cs rename to Text-Grab.Core/Models/TemplateRecognizerMatch.cs diff --git a/Text-Grab/Models/ThirdPartyPackageInfo.cs b/Text-Grab.Core/Models/ThirdPartyPackageInfo.cs similarity index 100% rename from Text-Grab/Models/ThirdPartyPackageInfo.cs rename to Text-Grab.Core/Models/ThirdPartyPackageInfo.cs diff --git a/Text-Grab.Core/Utilities/IoUtilities.cs b/Text-Grab.Core/Utilities/IoUtilities.cs index aa2bab6d..3763a565 100644 --- a/Text-Grab.Core/Utilities/IoUtilities.cs +++ b/Text-Grab.Core/Utilities/IoUtilities.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using Text_Grab.Models; namespace Text_Grab.Utilities; @@ -73,6 +74,19 @@ public static bool IsSpreadsheetFileExtension(string extension) return SpreadsheetExtensions.Contains(extension.ToLowerInvariant()); } + public static EtwEditorMode GetEditorModeForPath(string? path) + { + string extension = Path.GetExtension(path ?? string.Empty); + + if (IsSpreadsheetFileExtension(extension)) + return EtwEditorMode.Spreadsheet; + + if (IsMarkdownFileExtension(extension)) + return EtwEditorMode.Markdown; + + return EtwEditorMode.Text; + } + public static OpenContentKind GetOpenContentKindForPath(string? path) { string extension = Path.GetExtension(path ?? string.Empty); diff --git a/Text-Grab/Utilities/FileOpenUtilities.cs b/Text-Grab/Utilities/FileOpenUtilities.cs index 2ba06652..00cd3ed3 100644 --- a/Text-Grab/Utilities/FileOpenUtilities.cs +++ b/Text-Grab/Utilities/FileOpenUtilities.cs @@ -3,25 +3,11 @@ using System.Text; using System.Threading.Tasks; using Text_Grab.Interfaces; -using Text_Grab.Models; namespace Text_Grab.Utilities; public class FileOpenUtilities { - public static EtwEditorMode GetEditorModeForPath(string? path) - { - string extension = Path.GetExtension(path ?? string.Empty); - - if (IoUtilities.IsSpreadsheetFileExtension(extension)) - return EtwEditorMode.Spreadsheet; - - if (IoUtilities.IsMarkdownFileExtension(extension)) - return EtwEditorMode.Markdown; - - return EtwEditorMode.Text; - } - public static async Task<(string TextContent, OpenContentKind SourceKindOfContent)> GetContentFromPath(string pathOfFileToOpen, bool isMultipleFiles = false, ILanguage? language = null) { StringBuilder stringBuilder = new(); diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index dbce2a9d..b793b8aa 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -2529,7 +2529,7 @@ internal async void OpenPath(string pathOfFileToOpen, bool isMultipleFiles = fal { EtwEditorMode targetMode = isMultipleFiles ? EtwEditorMode.Text - : FileOpenUtilities.GetEditorModeForPath(pathOfFileToOpen); + : IoUtilities.GetEditorModeForPath(pathOfFileToOpen); if (IsLoaded) SetEditorMode(targetMode); From f6a1cd08cc2b2c291b0af09505384326234d2366 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:29:19 -0500 Subject: [PATCH 17/54] Move seven Utilities/Extensions/Interfaces files into Core, split two more Batch 2c. All nine files read in full per invariant 8. Straight moves (PatternExecutor first, then ColumnSplitUtilities which calls it): PatternExecutor, ColumnSplitUtilities, NumericUtilities, LanguageHeuristics (internal - already covered by Core's existing InternalsVisibleTo("Text-Grab")/("Tests")), Extensions/NumberExtensions, Extensions/StringBuilderExtensions (uses StringMethods, already in Core), Interfaces/ITtsEngine. No call sites needed fixing. Two splits: - ProtocolUtilities: IsProtocolUri/TryParseProtocolUri and the Scheme const are pure and keep the name in Core. TryGetSafeProtocolFilePath (needs AutomationProfile) and EnsureProtocolRegistration (needs Registry + FileUtilities) - both still app-side - moved into a new Text-Grab/Utilities/ProtocolHandlerUtilities.cs. Updated call sites: App.xaml.cs and the impure half of Tests/ProtocolUtilitiesTests.cs (the pure-half tests are untouched and still compile against Core through the existing InternalsVisibleTo; migrating them to Tests.Core is Wave 7a, not this batch). - ThirdPartyNoticeUtilities: the Packages catalog and its constants are pure and keep the name in Core. GetBuiltWithFilePath/ GetNoticesDirectoryPath/GetNoticeTarget/Open* all need FileUtilities.GetExePath() and moved into a new Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs. Updated the one call site, LicensesWindow.xaml.cs. All three existing tests touch only Packages, so ThirdPartyNoticeUtilitiesTests.cs moved to Tests.Core in this same commit (namespace updated to Text_Grab.Tests.Core to match that project's convention). Gate: Tests.Core 17/17 (14 baseline + 3 migrated), Tests.Core.Windows 3/3, Tests 1490/7 skipped/0 failed (1493 baseline - 3 migrated) - matches baseline net of the test migration. Co-Authored-By: Claude Opus 5 --- .../ThirdPartyNoticeUtilitiesTests.cs | 2 +- Tests/ProtocolUtilitiesTests.cs | 12 ++-- .../Extensions/NumberExtensions.cs | 0 .../Extensions/StringBuilderExtensions.cs | 0 .../Interfaces/ITtsEngine.cs | 0 .../Utilities/ColumnSplitUtilities.cs | 0 .../Utilities/LanguageHeuristics.cs | 0 .../Utilities/NumericUtilities.cs | 0 .../Utilities/PatternExecutor.cs | 0 Text-Grab.Core/Utilities/ProtocolUtilities.cs | 72 +++++++++++++++++++ .../Utilities/ThirdPartyNoticeUtilities.cs | 51 ++----------- Text-Grab/App.xaml.cs | 6 +- ...ilities.cs => ProtocolHandlerUtilities.cs} | 68 ++---------------- .../Utilities/ThirdPartyNoticeLauncher.cs | 58 +++++++++++++++ Text-Grab/Views/LicensesWindow.xaml.cs | 8 +-- 15 files changed, 156 insertions(+), 121 deletions(-) rename {Tests => Tests.Core}/ThirdPartyNoticeUtilitiesTests.cs (98%) rename {Text-Grab => Text-Grab.Core}/Extensions/NumberExtensions.cs (100%) rename {Text-Grab => Text-Grab.Core}/Extensions/StringBuilderExtensions.cs (100%) rename {Text-Grab => Text-Grab.Core}/Interfaces/ITtsEngine.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/ColumnSplitUtilities.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/LanguageHeuristics.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/NumericUtilities.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/PatternExecutor.cs (100%) create mode 100644 Text-Grab.Core/Utilities/ProtocolUtilities.cs rename {Text-Grab => Text-Grab.Core}/Utilities/ThirdPartyNoticeUtilities.cs (77%) rename Text-Grab/Utilities/{ProtocolUtilities.cs => ProtocolHandlerUtilities.cs} (69%) create mode 100644 Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs diff --git a/Tests/ThirdPartyNoticeUtilitiesTests.cs b/Tests.Core/ThirdPartyNoticeUtilitiesTests.cs similarity index 98% rename from Tests/ThirdPartyNoticeUtilitiesTests.cs rename to Tests.Core/ThirdPartyNoticeUtilitiesTests.cs index 84b896f3..f7cf3ff2 100644 --- a/Tests/ThirdPartyNoticeUtilitiesTests.cs +++ b/Tests.Core/ThirdPartyNoticeUtilitiesTests.cs @@ -1,7 +1,7 @@ using System.Linq; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; public class ThirdPartyNoticeUtilitiesTests { diff --git a/Tests/ProtocolUtilitiesTests.cs b/Tests/ProtocolUtilitiesTests.cs index 9bd0ce64..c6f4945f 100644 --- a/Tests/ProtocolUtilitiesTests.cs +++ b/Tests/ProtocolUtilitiesTests.cs @@ -126,7 +126,7 @@ public void TryGetSafeProtocolFilePath_AcceptsImageInTempFolder() File.WriteAllBytes(tempImage, [0]); try { - bool safe = ProtocolUtilities.TryGetSafeProtocolFilePath(tempImage, out string fullPath); + bool safe = ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(tempImage, out string fullPath); Assert.True(safe); Assert.Equal(Path.GetFullPath(tempImage), fullPath); @@ -147,7 +147,7 @@ public void TryGetSafeProtocolFilePath_AcceptsImageInTempFolder() [InlineData(@"\\.\PhysicalDrive0")] // device namespace public void TryGetSafeProtocolFilePath_RejectsUncDeviceAndEmptyPaths(string? path) { - Assert.False(ProtocolUtilities.TryGetSafeProtocolFilePath(path, out _)); + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(path, out _)); } [Fact] @@ -159,7 +159,7 @@ public void TryGetSafeProtocolFilePath_RejectsPathOutsideAllowedRoots() Environment.GetFolderPath(Environment.SpecialFolder.Windows), $"text-grab-{Guid.NewGuid():N}.png"); - Assert.False(ProtocolUtilities.TryGetSafeProtocolFilePath(outside, out _)); + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(outside, out _)); } [Fact] @@ -168,7 +168,7 @@ public void TryGetSafeProtocolFilePath_RejectsTraversalEscapingAllowedRoot() // Starts inside Temp but climbs out to the Windows folder. string traversal = Path.Combine(Path.GetTempPath(), "..", "..", "..", "Windows", "image.png"); - Assert.False(ProtocolUtilities.TryGetSafeProtocolFilePath(traversal, out _)); + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(traversal, out _)); } [Fact] @@ -178,7 +178,7 @@ public void TryGetSafeProtocolFilePath_RejectsNonImageExtensionInAllowedRoot() File.WriteAllText(tempText, "hello"); try { - Assert.False(ProtocolUtilities.TryGetSafeProtocolFilePath(tempText, out _)); + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(tempText, out _)); } finally { @@ -191,6 +191,6 @@ public void TryGetSafeProtocolFilePath_RejectsNonexistentImageInAllowedRoot() { string missing = Path.Combine(Path.GetTempPath(), $"text-grab-missing-{Guid.NewGuid():N}.png"); - Assert.False(ProtocolUtilities.TryGetSafeProtocolFilePath(missing, out _)); + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(missing, out _)); } } diff --git a/Text-Grab/Extensions/NumberExtensions.cs b/Text-Grab.Core/Extensions/NumberExtensions.cs similarity index 100% rename from Text-Grab/Extensions/NumberExtensions.cs rename to Text-Grab.Core/Extensions/NumberExtensions.cs diff --git a/Text-Grab/Extensions/StringBuilderExtensions.cs b/Text-Grab.Core/Extensions/StringBuilderExtensions.cs similarity index 100% rename from Text-Grab/Extensions/StringBuilderExtensions.cs rename to Text-Grab.Core/Extensions/StringBuilderExtensions.cs diff --git a/Text-Grab/Interfaces/ITtsEngine.cs b/Text-Grab.Core/Interfaces/ITtsEngine.cs similarity index 100% rename from Text-Grab/Interfaces/ITtsEngine.cs rename to Text-Grab.Core/Interfaces/ITtsEngine.cs diff --git a/Text-Grab/Utilities/ColumnSplitUtilities.cs b/Text-Grab.Core/Utilities/ColumnSplitUtilities.cs similarity index 100% rename from Text-Grab/Utilities/ColumnSplitUtilities.cs rename to Text-Grab.Core/Utilities/ColumnSplitUtilities.cs diff --git a/Text-Grab/Utilities/LanguageHeuristics.cs b/Text-Grab.Core/Utilities/LanguageHeuristics.cs similarity index 100% rename from Text-Grab/Utilities/LanguageHeuristics.cs rename to Text-Grab.Core/Utilities/LanguageHeuristics.cs diff --git a/Text-Grab/Utilities/NumericUtilities.cs b/Text-Grab.Core/Utilities/NumericUtilities.cs similarity index 100% rename from Text-Grab/Utilities/NumericUtilities.cs rename to Text-Grab.Core/Utilities/NumericUtilities.cs diff --git a/Text-Grab/Utilities/PatternExecutor.cs b/Text-Grab.Core/Utilities/PatternExecutor.cs similarity index 100% rename from Text-Grab/Utilities/PatternExecutor.cs rename to Text-Grab.Core/Utilities/PatternExecutor.cs diff --git a/Text-Grab.Core/Utilities/ProtocolUtilities.cs b/Text-Grab.Core/Utilities/ProtocolUtilities.cs new file mode 100644 index 00000000..e3886792 --- /dev/null +++ b/Text-Grab.Core/Utilities/ProtocolUtilities.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace Text_Grab.Utilities; + +/// +/// Pure parsing half of the text-grab:// protocol used by companion apps such as +/// the Text Grab browser extension. The URI is only a command channel; any +/// data payload (like a copied table) travels via the clipboard. +/// Supported URIs: +/// text-grab://paste-spreadsheet Edit Text window in spreadsheet mode, paste clipboard +/// text-grab://edit-text Edit Text window with clipboard text +/// text-grab://grab-frame[?path=...] Grab Frame, optionally opening a local image/PDF +/// text-grab://grab-text?path=... OCR a local image/PDF straight to the clipboard (no window) +/// text-grab://fullscreen Fullscreen grab +/// text-grab://quick-lookup Quick Simple Lookup +/// text-grab://settings Settings window +/// +/// Validating a companion app's path= parameter and registering the protocol with the +/// OS need Registry access, AutomationProfile and FileUtilities, so those methods +/// stay behind in Text-Grab/Utilities/ProtocolHandlerUtilities.cs. +/// +internal static class ProtocolUtilities +{ + internal const string Scheme = "text-grab"; + + /// + /// Returns true when a startup argument looks like a text-grab:// URI. + /// + internal static bool IsProtocolUri(string? argument) + { + return argument is not null + && argument.StartsWith($"{Scheme}:", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Parses a text-grab:// URI into a lowercase command and its query parameters. + /// Accepts both text-grab://command?key=value and text-grab:command forms. + /// + internal static bool TryParseProtocolUri( + string uriString, + out string command, + out Dictionary parameters) + { + command = string.Empty; + parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (!Uri.TryCreate(uriString, UriKind.Absolute, out Uri? uri) + || !string.Equals(uri.Scheme, Scheme, StringComparison.OrdinalIgnoreCase)) + return false; + + // text-grab://paste-spreadsheet puts the command in Host; + // text-grab:paste-spreadsheet puts it in AbsolutePath. + string rawCommand = !string.IsNullOrEmpty(uri.Host) ? uri.Host : uri.AbsolutePath; + command = rawCommand.Trim('/').ToLowerInvariant(); + if (string.IsNullOrEmpty(command)) + return false; + + string query = uri.Query.TrimStart('?'); + foreach (string pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + int separatorIndex = pair.IndexOf('='); + if (separatorIndex <= 0) + continue; + string key = Uri.UnescapeDataString(pair[..separatorIndex]); + string value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]); + parameters[key] = value; + } + + return true; + } +} diff --git a/Text-Grab/Utilities/ThirdPartyNoticeUtilities.cs b/Text-Grab.Core/Utilities/ThirdPartyNoticeUtilities.cs similarity index 77% rename from Text-Grab/Utilities/ThirdPartyNoticeUtilities.cs rename to Text-Grab.Core/Utilities/ThirdPartyNoticeUtilities.cs index 800fd217..3e76b6a3 100644 --- a/Text-Grab/Utilities/ThirdPartyNoticeUtilities.cs +++ b/Text-Grab.Core/Utilities/ThirdPartyNoticeUtilities.cs @@ -1,11 +1,13 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using Text_Grab.Models; namespace Text_Grab.Utilities; +/// +/// Pure package catalog. Resolving notice/license file paths against the running +/// executable's location and opening them needs FileUtilities.GetExePath(), so those +/// methods stay behind in Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs. +/// public static class ThirdPartyNoticeUtilities { public const string BuiltWithFileName = "BUILT-WITH.md"; @@ -44,47 +46,4 @@ public static class ThirdPartyNoticeUtilities new("Xunit.StaFact", "3.0.13", "Tests", "MS-PL", "https://github.com/AArnott/Xunit.StaFact", "https://github.com/AArnott/Xunit.StaFact/blob/main/LICENSE", false, "Test-only dependency."), new("xunit.v3", "3.2.2", "Tests", "Apache-2.0", "https://github.com/xunit/xunit", "https://github.com/xunit/xunit/blob/main/LICENSE", false, "Test-only dependency."), ]; - - public static string? GetBuiltWithFilePath() - { - string? executableDirectory = Path.GetDirectoryName(FileUtilities.GetExePath()); - return string.IsNullOrWhiteSpace(executableDirectory) - ? null - : Path.Combine(executableDirectory, BuiltWithFileName); - } - - public static string? GetNoticesDirectoryPath() - { - string? executableDirectory = Path.GetDirectoryName(FileUtilities.GetExePath()); - return string.IsNullOrWhiteSpace(executableDirectory) - ? null - : Path.Combine(executableDirectory, NoticesDirectoryName); - } - - public static string? GetNoticeTarget(ThirdPartyPackageInfo package) - { - if (!package.NoticeIsLocal) - return package.NoticeTarget; - - string? executableDirectory = Path.GetDirectoryName(FileUtilities.GetExePath()); - return string.IsNullOrWhiteSpace(executableDirectory) - ? null - : Path.Combine(executableDirectory, package.NoticeTarget); - } - - public static void OpenBuiltWithFile() => OpenTarget(GetBuiltWithFilePath()); - - public static void OpenNoticesDirectory() => OpenTarget(GetNoticesDirectoryPath()); - - public static void OpenNoticeFile(ThirdPartyPackageInfo package) => OpenTarget(GetNoticeTarget(package)); - - public static void OpenProjectUrl(ThirdPartyPackageInfo package) => OpenTarget(package.ProjectUrl); - - private static void OpenTarget(string? target) - { - if (string.IsNullOrWhiteSpace(target)) - return; - - Process.Start(new ProcessStartInfo(target) { UseShellExecute = true }); - } } diff --git a/Text-Grab/App.xaml.cs b/Text-Grab/App.xaml.cs index a27d2dd6..33e2acba 100644 --- a/Text-Grab/App.xaml.cs +++ b/Text-Grab/App.xaml.cs @@ -413,7 +413,7 @@ internal static bool HandleProtocolUri(string uriString) // open the file; an unsafe path falls back to an empty Grab Frame. if (parameters.TryGetValue("path", out string? path)) { - if (ProtocolUtilities.TryGetSafeProtocolFilePath(path, out string safePath)) + if (ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(path, out string safePath)) { GrabFrame gfWithFile = new(safePath); gfWithFile.Show(); @@ -434,7 +434,7 @@ internal static bool HandleProtocolUri(string uriString) // OCR a local image/PDF straight to the clipboard, no window. The path is // untrusted; only proceed for a validated, allowed local file. if (parameters.TryGetValue("path", out string? path) - && ProtocolUtilities.TryGetSafeProtocolFilePath(path, out string safePath)) + && ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(path, out string safePath)) { _ = GrabTextFromFileAsync(safePath); return true; @@ -600,7 +600,7 @@ private async void appStartup(object sender, StartupEventArgs e) // (packaged installs register these via the MSIX manifest). if (_automationProfile is null || _automationProfile.AllowsPersistentRegistration) { - ProtocolUtilities.EnsureProtocolRegistration(); + ProtocolHandlerUtilities.EnsureProtocolRegistration(); FileAssociationUtilities.EnsureGrabFrameFileAssociation(); } diff --git a/Text-Grab/Utilities/ProtocolUtilities.cs b/Text-Grab/Utilities/ProtocolHandlerUtilities.cs similarity index 69% rename from Text-Grab/Utilities/ProtocolUtilities.cs rename to Text-Grab/Utilities/ProtocolHandlerUtilities.cs index 27500114..8be68b25 100644 --- a/Text-Grab/Utilities/ProtocolUtilities.cs +++ b/Text-Grab/Utilities/ProtocolHandlerUtilities.cs @@ -7,69 +7,15 @@ namespace Text_Grab.Utilities; /// -/// Utility class for the text-grab:// protocol used by companion apps such as -/// the Text Grab browser extension. The URI is only a command channel; any -/// data payload (like a copied table) travels via the clipboard. -/// Supported URIs: -/// text-grab://paste-spreadsheet Edit Text window in spreadsheet mode, paste clipboard -/// text-grab://edit-text Edit Text window with clipboard text -/// text-grab://grab-frame[?path=...] Grab Frame, optionally opening a local image/PDF -/// text-grab://grab-text?path=... OCR a local image/PDF straight to the clipboard (no window) -/// text-grab://fullscreen Fullscreen grab -/// text-grab://quick-lookup Quick Simple Lookup -/// text-grab://settings Settings window +/// Impure half of the text-grab:// protocol handling that stayed split out of Core in batch 2c: +/// validating a companion app's path= parameter against the filesystem/AutomationProfile, +/// and registering the protocol with the OS. and +/// (the pure URI parsing) live in +/// Text-Grab.Core's under the original name. /// -internal static class ProtocolUtilities +internal static class ProtocolHandlerUtilities { - internal const string Scheme = "text-grab"; - - private const string ProtocolKeyPath = @"Software\Classes\" + Scheme; - - /// - /// Returns true when a startup argument looks like a text-grab:// URI. - /// - internal static bool IsProtocolUri(string? argument) - { - return argument is not null - && argument.StartsWith($"{Scheme}:", StringComparison.OrdinalIgnoreCase); - } - - /// - /// Parses a text-grab:// URI into a lowercase command and its query parameters. - /// Accepts both text-grab://command?key=value and text-grab:command forms. - /// - internal static bool TryParseProtocolUri( - string uriString, - out string command, - out Dictionary parameters) - { - command = string.Empty; - parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (!Uri.TryCreate(uriString, UriKind.Absolute, out Uri? uri) - || !string.Equals(uri.Scheme, Scheme, StringComparison.OrdinalIgnoreCase)) - return false; - - // text-grab://paste-spreadsheet puts the command in Host; - // text-grab:paste-spreadsheet puts it in AbsolutePath. - string rawCommand = !string.IsNullOrEmpty(uri.Host) ? uri.Host : uri.AbsolutePath; - command = rawCommand.Trim('/').ToLowerInvariant(); - if (string.IsNullOrEmpty(command)) - return false; - - string query = uri.Query.TrimStart('?'); - foreach (string pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries)) - { - int separatorIndex = pair.IndexOf('='); - if (separatorIndex <= 0) - continue; - string key = Uri.UnescapeDataString(pair[..separatorIndex]); - string value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]); - parameters[key] = value; - } - - return true; - } + private const string ProtocolKeyPath = @"Software\Classes\" + ProtocolUtilities.Scheme; /// /// Validates a path= parameter supplied via the text-grab:// protocol and, diff --git a/Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs b/Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs new file mode 100644 index 00000000..d526ff06 --- /dev/null +++ b/Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; +using System.IO; +using Text_Grab.Models; + +namespace Text_Grab.Utilities; + +/// +/// Impure half of the third-party notice utilities that stayed split out of Core in batch 2c: +/// resolving notice/license file paths against the running executable's location +/// () and opening them. The pure package catalog +/// () lives in Text-Grab.Core under the +/// original name. +/// +public static class ThirdPartyNoticeLauncher +{ + public static string? GetBuiltWithFilePath() + { + string? executableDirectory = Path.GetDirectoryName(FileUtilities.GetExePath()); + return string.IsNullOrWhiteSpace(executableDirectory) + ? null + : Path.Combine(executableDirectory, ThirdPartyNoticeUtilities.BuiltWithFileName); + } + + public static string? GetNoticesDirectoryPath() + { + string? executableDirectory = Path.GetDirectoryName(FileUtilities.GetExePath()); + return string.IsNullOrWhiteSpace(executableDirectory) + ? null + : Path.Combine(executableDirectory, ThirdPartyNoticeUtilities.NoticesDirectoryName); + } + + public static string? GetNoticeTarget(ThirdPartyPackageInfo package) + { + if (!package.NoticeIsLocal) + return package.NoticeTarget; + + string? executableDirectory = Path.GetDirectoryName(FileUtilities.GetExePath()); + return string.IsNullOrWhiteSpace(executableDirectory) + ? null + : Path.Combine(executableDirectory, package.NoticeTarget); + } + + public static void OpenBuiltWithFile() => OpenTarget(GetBuiltWithFilePath()); + + public static void OpenNoticesDirectory() => OpenTarget(GetNoticesDirectoryPath()); + + public static void OpenNoticeFile(ThirdPartyPackageInfo package) => OpenTarget(GetNoticeTarget(package)); + + public static void OpenProjectUrl(ThirdPartyPackageInfo package) => OpenTarget(package.ProjectUrl); + + private static void OpenTarget(string? target) + { + if (string.IsNullOrWhiteSpace(target)) + return; + + Process.Start(new ProcessStartInfo(target) { UseShellExecute = true }); + } +} diff --git a/Text-Grab/Views/LicensesWindow.xaml.cs b/Text-Grab/Views/LicensesWindow.xaml.cs index deb099d8..4903de95 100644 --- a/Text-Grab/Views/LicensesWindow.xaml.cs +++ b/Text-Grab/Views/LicensesWindow.xaml.cs @@ -19,23 +19,23 @@ public LicensesWindow() private void BuiltWithButton_Click(object sender, RoutedEventArgs e) { - ThirdPartyNoticeUtilities.OpenBuiltWithFile(); + ThirdPartyNoticeLauncher.OpenBuiltWithFile(); } private void NoticesFolderButton_Click(object sender, RoutedEventArgs e) { - ThirdPartyNoticeUtilities.OpenNoticesDirectory(); + ThirdPartyNoticeLauncher.OpenNoticesDirectory(); } private void NoticeButton_Click(object sender, RoutedEventArgs e) { if (sender is FrameworkElement { DataContext: ThirdPartyPackageInfo package }) - ThirdPartyNoticeUtilities.OpenNoticeFile(package); + ThirdPartyNoticeLauncher.OpenNoticeFile(package); } private void ProjectButton_Click(object sender, RoutedEventArgs e) { if (sender is FrameworkElement { DataContext: ThirdPartyPackageInfo package }) - ThirdPartyNoticeUtilities.OpenProjectUrl(package); + ThirdPartyNoticeLauncher.OpenProjectUrl(package); } } From f05be11b07fe59fb4080db41a233dc65f1263e7f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:37:49 -0500 Subject: [PATCH 18/54] Convert WordBorderInfo/TemplateRegion to RectangleF, move both plus GrabTemplate/OcrDirectoryOptions into Core Batch 2d - the geometry-currency payoff from B2 (30af90f). - WordBorderInfo.BorderRect: System.Windows.Rect -> System.Drawing.RectangleF. The WordBorder(WordBorder) constructor was the class's only WPF tie and moved out to a new app-side Text-Grab/Models/WordBorderInfoFactory.cs (WordBorderInfoFactory.Create(WordBorder)), same shape as PatternItem/ PatternItemCatalog in e677b54. Default changed from Rect.Empty to RectangleF.Empty - verified safe: grepped every BorderRect-adjacent emptiness check in the app and found none; all the `== Rect.Empty` call sites in the repo test PositionRect/CaptureRegion/other Rects, never BorderRect. - TemplateRegion.ToAbsoluteRect/FromAbsoluteRect: Rect -> RectangleF. Its only production call site (GrabFrame.xaml.cs:283) converts back with .AsRect(). FromAbsoluteRect has zero call sites anywhere in the repo (verified by grep, not just usings) - its signature changed anyway since it's public API on a moved type. - GrabTemplate.cs and OcrDirectoryOptions.cs: moved as pure renames, no content changes. Both were already clean once their TemplateRegion dependency could move. Call sites fixed (all compiler-flagged, no unrequested edits): - Text-Grab/Models/ResultTable.cs: BorderRect construction now uses lineRect.AsRectangleF(); two Median() calls needed an explicit (double) cast since IEnumerable no longer flows into an IEnumerable param. - Text-Grab/Views/GrabFrame.xaml.cs: three `new WordBorderInfo(wb)` call sites -> WordBorderInfoFactory.Create(wb); the TemplateRegion.ToAbsoluteRect call and the history-rescale block convert at the boundary with .AsRect()/ .AsRectangleF() rather than widening GrabFrame's own Rect usage. - Tests/*.cs (GrabFrameFileTests, HistoryServiceTests, ResultTableBenchmarks, ResultTableManualSeparatorTests): BorderRect literals changed from `new Rect(...)` to `new RectangleF(...)`; two of these needed explicit double->float casts on their arguments (precision-loss risk, see below). Removed now-unused `using Rect = System.Windows.Rect;` and `using System.Windows;` where nothing else in the file needed them. Semantic risk (compiles and passes tests, flagging per instructions): - double->float precision loss at every BorderRect write site: the ResultTableBenchmarks/ResultTableManualSeparatorTests literals, the WordBorderInfoFactory conversion from WPF Left/Top/Width/Height (double), and the GrabFrame history-rescale multiply-then-AsRectangleF() path. All are screen/OCR pixel coordinates well within float's exact-integer range (2^24), so this is not expected to be visible, but it is a real behavior change from the previous all-double path. - BorderRect's default changed from Rect.Empty (WPF: NaN-based, IsEmpty semantics) to RectangleF.Empty (all-zeros). No current call site tests BorderRect for emptiness either way, so this is currently inert, but a future `== RectangleF.Empty` check on a legitimately-zero-sized rect would behave differently than the old `== Rect.Empty` would have on a legitimately-empty one. - ParseOcrResultIntoWordBorderInfos still builds its intermediate lineRect as a WPF Rect (unrelated OCR math untouched) and only converts to RectangleF at the final assignment - kept for minimal diff, not a behavior change. Deferred (not in this batch's file list, left alone per invariant 5): - Utilities/GrabTemplateManager.cs and Utilities/GrabTemplateExecutor.cs still have real blockers (BitmapSource/Wpf.Ui in the manager; a non-scalar LoadStoredRegexes() seam and the OCR half's Wave-3/4c dependency in the executor) unrelated to TemplateRegion. Updated their section 7 rows to drop the now-resolved "template models not in Core" clause. Gate: Tests.Core 17/17, Tests.Core.Windows 3/3, Tests 1490 passed / 7 skipped / 0 failed - matches baseline exactly. Co-Authored-By: Claude Opus 5 --- Tests/GrabFrameFileTests.cs | 6 +-- Tests/HistoryServiceTests.cs | 3 +- Tests/ResultTableBenchmarks.cs | 3 +- Tests/ResultTableManualSeparatorTests.cs | 3 +- .../Models/GrabTemplate.cs | 0 .../Models/OcrDirectoryOptions.cs | 0 .../Models/TemplateRegion.cs | 20 ++++---- Text-Grab.Core/Models/WordBorderInfo.cs | 22 +++++++++ Text-Grab/Models/ResultTable.cs | 6 +-- Text-Grab/Models/WordBorderInfo.cs | 46 ------------------- Text-Grab/Models/WordBorderInfoFactory.cs | 37 +++++++++++++++ Text-Grab/Views/GrabFrame.xaml.cs | 12 ++--- docs/Core-Split-Plan.md | 4 +- 13 files changed, 87 insertions(+), 75 deletions(-) rename {Text-Grab => Text-Grab.Core}/Models/GrabTemplate.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/OcrDirectoryOptions.cs (100%) rename {Text-Grab => Text-Grab.Core}/Models/TemplateRegion.cs (75%) create mode 100644 Text-Grab.Core/Models/WordBorderInfo.cs delete mode 100644 Text-Grab/Models/WordBorderInfo.cs create mode 100644 Text-Grab/Models/WordBorderInfoFactory.cs diff --git a/Tests/GrabFrameFileTests.cs b/Tests/GrabFrameFileTests.cs index 0ec2647b..cf0bfa81 100644 --- a/Tests/GrabFrameFileTests.cs +++ b/Tests/GrabFrameFileTests.cs @@ -21,13 +21,13 @@ public async Task SaveAndLoad_RoundTripsMetadataWordBordersAndImage() new() { Word = "Hello", - BorderRect = new Rect(1, 2, 30, 12), + BorderRect = new RectangleF(1, 2, 30, 12), LineNumber = 0, }, new() { Word = "World", - BorderRect = new Rect(35, 2, 32, 12), + BorderRect = new RectangleF(35, 2, 32, 12), LineNumber = 0, }, ]; @@ -91,7 +91,7 @@ public async Task SaveGrabFrameFileAsync_DoesNotMutateSuppliedInfo() string originalWordBordersJson = JsonSerializer.Serialize(new List { - new() { Word = "Hello", BorderRect = new Rect(1, 2, 30, 12), LineNumber = 0 }, + new() { Word = "Hello", BorderRect = new RectangleF(1, 2, 30, 12), LineNumber = 0 }, }); Bitmap originalImage = new(64, 48); diff --git a/Tests/HistoryServiceTests.cs b/Tests/HistoryServiceTests.cs index a2153a53..36ca2cef 100644 --- a/Tests/HistoryServiceTests.cs +++ b/Tests/HistoryServiceTests.cs @@ -1,3 +1,4 @@ +using System.Drawing; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows; @@ -176,7 +177,7 @@ public async Task ImageHistory_KeepsInlineWordBorderJsonWhileMirroringSidecarSto { Word = "hello", DisplayText = $"hello{Environment.NewLine}world", - BorderRect = new Rect(1, 2, 30, 40), + BorderRect = new RectangleF(1, 2, 30, 40), DisplayLineHeight = 18, KeepSingleLineOutput = true, LineNumber = 1, diff --git a/Tests/ResultTableBenchmarks.cs b/Tests/ResultTableBenchmarks.cs index d12e0e80..7b64c34b 100644 --- a/Tests/ResultTableBenchmarks.cs +++ b/Tests/ResultTableBenchmarks.cs @@ -2,7 +2,6 @@ using System.Drawing; using System.Text; using Text_Grab.Models; -using Rect = System.Windows.Rect; namespace Tests.Benchmarks; @@ -50,7 +49,7 @@ public void Setup() WordBorderInfo w = new() { Word = token, - BorderRect = new Rect(curLeft, top, Math.Max(12, token.Length * 7), rowH) + BorderRect = new RectangleF((float)curLeft, (float)top, Math.Max(12, token.Length * 7), (float)rowH) }; _syntheticBorders.Add(w); curLeft += w.BorderRect.Width + gapX; diff --git a/Tests/ResultTableManualSeparatorTests.cs b/Tests/ResultTableManualSeparatorTests.cs index de777577..33905d4b 100644 --- a/Tests/ResultTableManualSeparatorTests.cs +++ b/Tests/ResultTableManualSeparatorTests.cs @@ -1,6 +1,5 @@ using System.Drawing; using System.Text; -using System.Windows; using Text_Grab.Models; namespace Tests; @@ -90,7 +89,7 @@ private static WordBorderInfo CreateWord(string word, double left, double top, d return new WordBorderInfo { Word = word, - BorderRect = new Rect(left, top, width, height) + BorderRect = new RectangleF((float)left, (float)top, (float)width, (float)height) }; } } diff --git a/Text-Grab/Models/GrabTemplate.cs b/Text-Grab.Core/Models/GrabTemplate.cs similarity index 100% rename from Text-Grab/Models/GrabTemplate.cs rename to Text-Grab.Core/Models/GrabTemplate.cs diff --git a/Text-Grab/Models/OcrDirectoryOptions.cs b/Text-Grab.Core/Models/OcrDirectoryOptions.cs similarity index 100% rename from Text-Grab/Models/OcrDirectoryOptions.cs rename to Text-Grab.Core/Models/OcrDirectoryOptions.cs diff --git a/Text-Grab/Models/TemplateRegion.cs b/Text-Grab.Core/Models/TemplateRegion.cs similarity index 75% rename from Text-Grab/Models/TemplateRegion.cs rename to Text-Grab.Core/Models/TemplateRegion.cs index f2a16117..b7a91cad 100644 --- a/Text-Grab/Models/TemplateRegion.cs +++ b/Text-Grab.Core/Models/TemplateRegion.cs @@ -1,4 +1,4 @@ -using System.Windows; +using System.Drawing; namespace Text_Grab.Models; @@ -37,21 +37,21 @@ public class TemplateRegion public TemplateRegion() { } /// - /// Returns the absolute pixel Rect for this region given the canvas/image dimensions. + /// Returns the absolute pixel rect for this region given the canvas/image dimensions. /// - public Rect ToAbsoluteRect(double imageWidth, double imageHeight) + public RectangleF ToAbsoluteRect(double imageWidth, double imageHeight) { - return new Rect( - x: RatioLeft * imageWidth, - y: RatioTop * imageHeight, - width: RatioWidth * imageWidth, - height: RatioHeight * imageHeight); + return new RectangleF( + x: (float)(RatioLeft * imageWidth), + y: (float)(RatioTop * imageHeight), + width: (float)(RatioWidth * imageWidth), + height: (float)(RatioHeight * imageHeight)); } /// - /// Sets ratio values from an absolute Rect and canvas dimensions. + /// Sets ratio values from an absolute rect and canvas dimensions. /// - public static TemplateRegion FromAbsoluteRect(Rect rect, double imageWidth, double imageHeight, int regionNumber, string label = "") + public static TemplateRegion FromAbsoluteRect(RectangleF rect, double imageWidth, double imageHeight, int regionNumber, string label = "") { return new TemplateRegion { diff --git a/Text-Grab.Core/Models/WordBorderInfo.cs b/Text-Grab.Core/Models/WordBorderInfo.cs new file mode 100644 index 00000000..d24ff56b --- /dev/null +++ b/Text-Grab.Core/Models/WordBorderInfo.cs @@ -0,0 +1,22 @@ +using System.Drawing; + +namespace Text_Grab.Models; + +public class WordBorderInfo +{ + public string Word { get; set; } = string.Empty; + public string DisplayText { get; set; } = string.Empty; + public RectangleF BorderRect { get; set; } = RectangleF.Empty; + public double DisplayLineHeight { get; set; } = 0; + public bool KeepSingleLineOutput { get; set; } = false; + public int LineNumber { get; set; } = 0; + public int ResultColumnID { get; set; } = 0; + public int ResultRowID { get; set; } = 0; + public string MatchingBackground { get; set; } = "Transparent"; + public bool IsBarcode { get; set; } = false; + + public WordBorderInfo() + { + + } +} diff --git a/Text-Grab/Models/ResultTable.cs b/Text-Grab/Models/ResultTable.cs index 60d48ff2..15ddc827 100644 --- a/Text-Grab/Models/ResultTable.cs +++ b/Text-Grab/Models/ResultTable.cs @@ -164,7 +164,7 @@ public static List ParseOcrResultIntoWordBorderInfos( WordBorderInfo info = new() { - BorderRect = new Rect(lineRect.X, lineRect.Y, lineRect.Width, lineRect.Height), + BorderRect = lineRect.AsRectangleF(), Word = lineText.ToString().Trim(), ResultRowID = 0, ResultColumnID = 0 @@ -206,11 +206,11 @@ public void AnalyzeAsTable( wb.Word = s.Trim(); } - double medianHeight = Median(wordBorders.Select(w => w.BorderRect.Height).Where(h => h > 0)); + double medianHeight = Median(wordBorders.Select(w => (double)w.BorderRect.Height).Where(h => h > 0)); if (double.IsNaN(medianHeight) || medianHeight <= 0) medianHeight = 20; double rowCenterThreshold = Math.Max(4, medianHeight * 0.75); - double medianWidth = Median(wordBorders.Select(w => w.BorderRect.Width).Where(w => w > 0)); + double medianWidth = Median(wordBorders.Select(w => (double)w.BorderRect.Width).Where(w => w > 0)); if (double.IsNaN(medianWidth) || medianWidth <= 0) medianWidth = 40; double columnCenterThreshold = Math.Max(24, medianWidth * 0.9); diff --git a/Text-Grab/Models/WordBorderInfo.cs b/Text-Grab/Models/WordBorderInfo.cs deleted file mode 100644 index 25e6934f..00000000 --- a/Text-Grab/Models/WordBorderInfo.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Windows; -using Text_Grab.Controls; - -namespace Text_Grab.Models; - -public class WordBorderInfo -{ - public string Word { get; set; } = string.Empty; - public string DisplayText { get; set; } = string.Empty; - public Rect BorderRect { get; set; } = Rect.Empty; - public double DisplayLineHeight { get; set; } = 0; - public bool KeepSingleLineOutput { get; set; } = false; - public int LineNumber { get; set; } = 0; - public int ResultColumnID { get; set; } = 0; - public int ResultRowID { get; set; } = 0; - public string MatchingBackground { get; set; } = "Transparent"; - public bool IsBarcode { get; set; } = false; - - public WordBorderInfo() - { - - } - - public WordBorderInfo(WordBorder wordBorder) - { - Word = wordBorder.Word; - DisplayText = wordBorder.KeepSingleLineOutput || !string.Equals(wordBorder.DisplayText, wordBorder.Word, StringComparison.Ordinal) - ? wordBorder.DisplayText - : string.Empty; - DisplayLineHeight = wordBorder.DisplayLineHeight; - KeepSingleLineOutput = wordBorder.KeepSingleLineOutput; - LineNumber = wordBorder.LineNumber; - ResultColumnID = wordBorder.ResultColumnID; - ResultRowID = wordBorder.ResultRowID; - MatchingBackground = wordBorder.MatchingBackground.ToString(); - IsBarcode = wordBorder.IsBarcode; - BorderRect = new() - { - X = wordBorder.Left, - Y = wordBorder.Top, - Width = wordBorder.Width, - Height = wordBorder.Height - }; - } -} diff --git a/Text-Grab/Models/WordBorderInfoFactory.cs b/Text-Grab/Models/WordBorderInfoFactory.cs new file mode 100644 index 00000000..762e4396 --- /dev/null +++ b/Text-Grab/Models/WordBorderInfoFactory.cs @@ -0,0 +1,37 @@ +using System; +using Text_Grab.Controls; + +namespace Text_Grab.Models; + +/// +/// Builds a pure projection from the WPF +/// control. Split out of so that class could move to +/// Text-Grab.Core — is a WPF control and can never follow it there. +/// +public static class WordBorderInfoFactory +{ + public static WordBorderInfo Create(WordBorder wordBorder) + { + return new WordBorderInfo + { + Word = wordBorder.Word, + DisplayText = wordBorder.KeepSingleLineOutput || !string.Equals(wordBorder.DisplayText, wordBorder.Word, StringComparison.Ordinal) + ? wordBorder.DisplayText + : string.Empty, + DisplayLineHeight = wordBorder.DisplayLineHeight, + KeepSingleLineOutput = wordBorder.KeepSingleLineOutput, + LineNumber = wordBorder.LineNumber, + ResultColumnID = wordBorder.ResultColumnID, + ResultRowID = wordBorder.ResultRowID, + MatchingBackground = wordBorder.MatchingBackground.ToString(), + IsBarcode = wordBorder.IsBarcode, + BorderRect = new() + { + X = (float)wordBorder.Left, + Y = (float)wordBorder.Top, + Width = (float)wordBorder.Width, + Height = (float)wordBorder.Height + } + }; + } +} diff --git a/Text-Grab/Views/GrabFrame.xaml.cs b/Text-Grab/Views/GrabFrame.xaml.cs index 73e378d0..5e780867 100644 --- a/Text-Grab/Views/GrabFrame.xaml.cs +++ b/Text-Grab/Views/GrabFrame.xaml.cs @@ -280,7 +280,7 @@ private async Task LoadTemplateForEditing(GrabTemplate template) foreach (TemplateRegion region in template.Regions.OrderBy(r => r.RegionNumber)) { - Rect abs = region.ToAbsoluteRect(cw, ch); + Rect abs = region.ToAbsoluteRect(cw, ch).AsRect(); WordBorder wb = new() { @@ -481,12 +481,12 @@ private void ScaleHistoryWordBordersToCanvas(HistoryInfo history, List 0) info.DisplayLineHeight *= scaleY; @@ -1217,7 +1217,7 @@ public HistoryInfo AsHistoryItem() List wbInfoList = []; foreach (WordBorder wb in wordBorders) - wbInfoList.Add(new WordBorderInfo(wb)); + wbInfoList.Add(WordBorderInfoFactory.Create(wb)); string? wbInfoJson = null; if (wbInfoList.Count > 0) @@ -1540,7 +1540,7 @@ public void MergeSelectedWordBorders() DpiScale dpi = VisualTreeHelper.GetDpi(this); // Build merged content via model-only ResultTable - List selInfos = [.. selectedWordBorders.Select(wb => new WordBorderInfo(wb))]; + List selInfos = [.. selectedWordBorders.Select(wb => WordBorderInfoFactory.Create(wb))]; ResultTable tmp = new(); tmp.AnalyzeAsTable(selInfos, new System.Drawing.Rectangle(0, 0, (int)ActualWidth, (int)ActualHeight)); StringBuilder sb = new(); @@ -4968,7 +4968,7 @@ private List TryToPlaceTable() { RemoveTableLines(); - List wbInfos = [.. wordBorders.Select(wb => new WordBorderInfo(wb))]; + List wbInfos = [.. wordBorders.Select(wb => WordBorderInfoFactory.Create(wb))]; if (wbInfos.Count == 0) { AnalyzedResultTable = null; diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 63316b04..361d4879 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -657,8 +657,8 @@ Every row below was verified by reading the file, not inferred. | `Utilities/FileUtilities.cs` | `AutomationProfile.Current` in 6 methods | 6a | | `Utilities/GrabFrameFileUtilities.cs` | Public signature bound to `HistoryInfo`, which needs B2 (`Rect PositionRect`), 2a (five enums) and 4e. A façade was considered and rejected as larger than the file it wraps | 2d + 4e, or `HistoryInfo` moving | | `Services/SettingsService.cs` | Clones `ButtonInfo` and `ShortcutKeySet` field-by-field (both never-move); `Windows.Storage.ApplicationDataContainer` caps it at Core.Windows regardless | needs a `ButtonInfo` redesign — likely never | -| `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar | 2d, then a split | -| `Utilities/GrabTemplateExecutor.cs` | Template models not in Core; `LoadStoredRegexes()` needs a non-scalar seam; OCR half blocked on 4c | 2d + 4c + a façade | +| `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so the remaining blocker is a plain split | a split | +| `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam; OCR half blocked on 4c. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so that part of the original blocker is resolved | 4c + a façade | | `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | | `Services/HistoryService.cs` | Headless JSON pipeline interleaved with WPF menu building and `GrabFrame`/`EditTextWindow` construction, sharing private state | Opus split (6e) | From 147ece4a246d37751c9ed41aac0b04b437905116 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:42:24 -0500 Subject: [PATCH 19/54] Move CalculationService into Text-Grab.Core Batch 2e - all three CalculationService files (~2000 lines) were fully pure: NCalc, UnitsNet, System.* only, plus one call into NumericUtilities.TryParseFlexibleDouble (already in Core as of 2c). No WPF/Windows types anywhere in the trio. Moved as a clean rename, namespace unchanged (Text_Grab.Services). - Text-Grab.Core/Services/CalculationService.cs - Text-Grab.Core/Services/CalculationService.DateTimeMath.cs - Text-Grab.Core/Services/CalculationService.UnitMath.cs Moved the NCalcAsync and UnitsNet PackageReferences from Text-Grab.csproj to Text-Grab.Core.csproj - repo-wide grep confirmed no other app file uses either package (BarcodeUtilitiesTests.cs has an unused `using UnitsNet;` but no UnitsNet types), so the app gets both transitively through the project reference. Left Tests.csproj's own NCalcAsync reference alone per the plan; Tests.Core needs no NCalcAsync reference of its own since it gets it transitively through Text-Grab.Core's new PackageReference. Moved CalculatorTests.cs and UnitConversionTests.cs from Tests/ to Tests.Core/ in the same commit, updating their namespace from `Tests` to `Text_Grab.Tests.Core` to match the convention ThirdPartyNoticeUtilitiesTests.cs established (f6a1cd0). No WPF dependencies found in either test file. Test counts, migration shift accounted for: - Tests.Core: 17 -> 487 (+470) - Tests.Core.Windows: 3 -> 3 (unchanged) - Tests: 1490 -> 1020 passed / 7 skipped (-470 passed, migrated out) 470 + 1020 = 1490, so no regression hid inside the shift. Co-Authored-By: Claude Opus 5 --- {Tests => Tests.Core}/CalculatorTests.cs | 2 +- {Tests => Tests.Core}/UnitConversionTests.cs | 2 +- .../Services/CalculationService.DateTimeMath.cs | 0 .../Services/CalculationService.UnitMath.cs | 0 {Text-Grab => Text-Grab.Core}/Services/CalculationService.cs | 0 Text-Grab.Core/Text-Grab.Core.csproj | 2 ++ Text-Grab/Text-Grab.csproj | 2 -- 7 files changed, 4 insertions(+), 4 deletions(-) rename {Tests => Tests.Core}/CalculatorTests.cs (99%) rename {Tests => Tests.Core}/UnitConversionTests.cs (99%) rename {Text-Grab => Text-Grab.Core}/Services/CalculationService.DateTimeMath.cs (100%) rename {Text-Grab => Text-Grab.Core}/Services/CalculationService.UnitMath.cs (100%) rename {Text-Grab => Text-Grab.Core}/Services/CalculationService.cs (100%) diff --git a/Tests/CalculatorTests.cs b/Tests.Core/CalculatorTests.cs similarity index 99% rename from Tests/CalculatorTests.cs rename to Tests.Core/CalculatorTests.cs index 5201a76d..622368ad 100644 --- a/Tests/CalculatorTests.cs +++ b/Tests.Core/CalculatorTests.cs @@ -3,7 +3,7 @@ using System.Globalization; using Text_Grab.Services; -namespace Tests; +namespace Text_Grab.Tests.Core; public class CalculatorTests { diff --git a/Tests/UnitConversionTests.cs b/Tests.Core/UnitConversionTests.cs similarity index 99% rename from Tests/UnitConversionTests.cs rename to Tests.Core/UnitConversionTests.cs index 1170dd15..05ec9c28 100644 --- a/Tests/UnitConversionTests.cs +++ b/Tests.Core/UnitConversionTests.cs @@ -1,6 +1,6 @@ using Text_Grab.Services; -namespace Tests; +namespace Text_Grab.Tests.Core; public class UnitConversionTests { diff --git a/Text-Grab/Services/CalculationService.DateTimeMath.cs b/Text-Grab.Core/Services/CalculationService.DateTimeMath.cs similarity index 100% rename from Text-Grab/Services/CalculationService.DateTimeMath.cs rename to Text-Grab.Core/Services/CalculationService.DateTimeMath.cs diff --git a/Text-Grab/Services/CalculationService.UnitMath.cs b/Text-Grab.Core/Services/CalculationService.UnitMath.cs similarity index 100% rename from Text-Grab/Services/CalculationService.UnitMath.cs rename to Text-Grab.Core/Services/CalculationService.UnitMath.cs diff --git a/Text-Grab/Services/CalculationService.cs b/Text-Grab.Core/Services/CalculationService.cs similarity index 100% rename from Text-Grab/Services/CalculationService.cs rename to Text-Grab.Core/Services/CalculationService.cs diff --git a/Text-Grab.Core/Text-Grab.Core.csproj b/Text-Grab.Core/Text-Grab.Core.csproj index 914dfad6..7a46fc0d 100644 --- a/Text-Grab.Core/Text-Grab.Core.csproj +++ b/Text-Grab.Core/Text-Grab.Core.csproj @@ -18,11 +18,13 @@ + none + diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index 1429d9e9..4ad22d04 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -120,14 +120,12 @@ - none - From 0000bc62732464779b6d886415f6b6e4c1ed298f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:51:14 -0500 Subject: [PATCH 20/54] Split MarkdownDocumentUtilities: pure AST/regex helpers into Core Batch 2f. About 150 pure lines out of 1168. Verified each of the plan's listed members individually against invariant 8 rather than trusting the list; all checked out as pure Markdig/regex/string code with no FlowDocument or other System.Windows.Documents types: LooksLikeMarkdown, ShouldPromoteLiveBlock, ShouldPromoteLiveMarkdown, NormalizeDocumentText, NormalizeNewlines, EscapeMarkdownText, EscapeLinkDestination, ApplyQuotePrefix, GetQuotePrefix, GetOrderedListStart, ResolveContentSpan, GetSourceSlice, GetCodeSpanContentRawStart, GetCodeBlockText, the MarkdownPipeline field, and the three [GeneratedRegex] partial methods. A partial class cannot span assemblies, so the split needed a new type name for the impure half, per the PatternItem/PatternItemCatalog shape from e677b54: - Text-Grab.Core/Utilities/MarkdownDocumentUtilities.cs keeps the original type name and namespace (Text_Grab.Utilities), holding only the members above. The three now-private-in-Core regex-backed static fields (LiveBlockTriggerRegex etc.) moved with the methods that use them. Previously-private members that the app-side facade still calls became `internal` (MarkdownPipeline field included) - Core already has InternalsVisibleTo("Text-Grab") from e677b54, so no new visibility plumbing was needed. - Text-Grab/Utilities/MarkdownFlowDocumentUtilities.cs (renamed from the original file) keeps everything FlowDocument-bound: CreateFlowDocument, SerializeToMarkdown, GetDocumentPlainText, BuildOffsetMap, MapRawOffsetToPosition and their private helpers, ApplyTheme, the AppendBlock/WriteBlock tree, and all the attached DependencyProperty plumbing (its typeof() owner arguments now point at the new type, since that's the class actually declaring them). Every call from here into the moved pure helpers is qualified with `MarkdownDocumentUtilities.`. Call sites fixed (all compiler-flagged after the rename, no unrequested edits): Text-Grab/Views/EditTextWindow.xaml.cs - the three pure calls (ShouldPromoteLiveMarkdown, ShouldPromoteLiveBlock, LooksLikeMarkdown) keep referring to MarkdownDocumentUtilities; the FlowDocument-touching calls (CreateFlowDocument, BuildOffsetMap, SerializeToMarkdown, ApplyTheme, MapRawOffsetToPosition, GetDocumentPlainText, the MarkdownOffsetMap field type) were renamed to MarkdownFlowDocumentUtilities. Markdig stays referenced by both halves, as the plan calls out: the app side pattern-matches on Markdig AST types directly in AppendBlock/AppendInline, so its own PackageReference had to stay - verified this is a real compile need, not just relying on transitivity, before leaving it in place. Added the same Markdig 1.3.2 PackageReference to Text-Grab.Core.csproj so the pure half's Markdig.Syntax/Markdig.Syntax.Inlines/Markdig.Extensions.AutoIdentifiers usages resolve. Extracted the 6 pure test methods (all [Theory], no WpfFact, no FlowDocument) into Tests.Core/MarkdownParsingTests.cs, namespace Text_Grab.Tests.Core matching the ThirdPartyNoticeUtilitiesTests convention. Renamed the remaining 33 FlowDocument-dependent call sites in Tests/MarkdownDocumentUtilitiesTests.cs from MarkdownDocumentUtilities to MarkdownFlowDocumentUtilities; mechanical, no other changes. Test counts, migration shift accounted for: - Tests.Core: 487 -> 514 (+27, the 6 Theory methods' InlineData cases: 6+3+5+4+6+3) - Tests.Core.Windows: 3 -> 3 (unchanged) - Tests: 1020 -> 993 passed / 7 skipped (-27 passed, migrated out) 27 + 993 = 1020, so no regression hid inside the shift. Co-Authored-By: Claude Opus 5 --- Tests.Core/MarkdownParsingTests.cs | 69 +++++ Tests/MarkdownDocumentUtilitiesTests.cs | 129 +++------ Text-Grab.Core/Text-Grab.Core.csproj | 1 + .../Utilities/MarkdownDocumentUtilities.cs | 176 ++++++++++++ ...es.cs => MarkdownFlowDocumentUtilities.cs} | 260 ++++-------------- Text-Grab/Views/EditTextWindow.xaml.cs | 22 +- 6 files changed, 339 insertions(+), 318 deletions(-) create mode 100644 Tests.Core/MarkdownParsingTests.cs create mode 100644 Text-Grab.Core/Utilities/MarkdownDocumentUtilities.cs rename Text-Grab/Utilities/{MarkdownDocumentUtilities.cs => MarkdownFlowDocumentUtilities.cs} (79%) diff --git a/Tests.Core/MarkdownParsingTests.cs b/Tests.Core/MarkdownParsingTests.cs new file mode 100644 index 00000000..07161832 --- /dev/null +++ b/Tests.Core/MarkdownParsingTests.cs @@ -0,0 +1,69 @@ +using Text_Grab.Utilities; + +namespace Text_Grab.Tests.Core; + +public class MarkdownParsingTests +{ + [Theory] + [InlineData("#")] + [InlineData("##")] + [InlineData(">")] + [InlineData(" >")] + [InlineData("-")] + [InlineData("1.")] + public void LiveBlockTriggerMarkers_AreRecognized(string marker) + { + Assert.True(MarkdownDocumentUtilities.ShouldPromoteLiveBlock(marker)); + } + + [Theory] + [InlineData("text")] + [InlineData("hello # world")] + [InlineData("1.2")] + public void NonTriggerText_DoesNotPromoteLiveBlock(string text) + { + Assert.False(MarkdownDocumentUtilities.ShouldPromoteLiveBlock(text)); + } + + [Theory] + [InlineData("**bold**")] + [InlineData("`code`")] + [InlineData("[link](https://example.com)")] + [InlineData("[ ] task")] + [InlineData("[x] done")] + public void CompletedMarkdownSyntax_PromotesLiveParsing(string text) + { + Assert.True(MarkdownDocumentUtilities.ShouldPromoteLiveMarkdown(text)); + } + + [Theory] + [InlineData("*")] + [InlineData("[link]")] + [InlineData("plain text")] + [InlineData("2026.04 release notes")] + public void IncompleteMarkdownSyntax_DoesNotPromoteLiveParsing(string text) + { + Assert.False(MarkdownDocumentUtilities.ShouldPromoteLiveMarkdown(text)); + } + + [Theory] + [InlineData("# Heading")] + [InlineData("> quote")] + [InlineData("- item")] + [InlineData("1. item")] + [InlineData("[link](https://example.com)")] + [InlineData("```csharp\nConsole.WriteLine(\"hi\");\n```")] + public void MarkdownLikeText_IsDetectedForPasteParsing(string text) + { + Assert.True(MarkdownDocumentUtilities.LooksLikeMarkdown(text)); + } + + [Theory] + [InlineData("Just a normal sentence.")] + [InlineData("2026.04 release notes")] + [InlineData("email me at joe@example.com")] + public void PlainText_IsNotDetectedAsMarkdown(string text) + { + Assert.False(MarkdownDocumentUtilities.LooksLikeMarkdown(text)); + } +} diff --git a/Tests/MarkdownDocumentUtilitiesTests.cs b/Tests/MarkdownDocumentUtilitiesTests.cs index 48929356..404d6d8e 100644 --- a/Tests/MarkdownDocumentUtilitiesTests.cs +++ b/Tests/MarkdownDocumentUtilitiesTests.cs @@ -24,9 +24,9 @@ public void Markdown_RoundTrips_CommonFormatting() ``` """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - string serialized = MarkdownDocumentUtilities.SerializeToMarkdown(document); + string serialized = MarkdownFlowDocumentUtilities.SerializeToMarkdown(document); Assert.Contains("# Heading", serialized); Assert.Contains("**bold**", serialized); @@ -47,9 +47,9 @@ public void Markdown_Tables_RoundTrip_ToPipeTable() | Beta | 99 | """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - string serialized = MarkdownDocumentUtilities.SerializeToMarkdown(document); + string serialized = MarkdownFlowDocumentUtilities.SerializeToMarkdown(document); Assert.Contains("| Name | Value |", serialized); Assert.Contains("| Alpha | 42 |", serialized); @@ -64,9 +64,9 @@ public void Markdown_TaskLists_RoundTrip_ToCheckboxMarkers() - [x] done item """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - string serialized = MarkdownDocumentUtilities.SerializeToMarkdown(document); + string serialized = MarkdownFlowDocumentUtilities.SerializeToMarkdown(document); Assert.Contains("- [ ] open item", serialized); Assert.Contains("- [x] done item", serialized); @@ -80,14 +80,14 @@ 5. fifth 6. sixth """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument( + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument( markdown, new FontFamily("Segoe UI"), 16); System.Windows.Documents.List list = Assert.IsType(Assert.Single(document.Blocks)); - string serialized = MarkdownDocumentUtilities.SerializeToMarkdown(document); + string serialized = MarkdownFlowDocumentUtilities.SerializeToMarkdown(document); Assert.Equal(5, list.StartIndex); Assert.Equal($"5. fifth{Environment.NewLine}6. sixth", serialized); @@ -99,7 +99,7 @@ public void PlainText_WithMarkdownCharacters_IsEscapedDuringSerialization() FlowDocument document = new(); document.Blocks.Add(new Paragraph(new Run("*literal* [value]"))); - string serialized = MarkdownDocumentUtilities.SerializeToMarkdown(document); + string serialized = MarkdownFlowDocumentUtilities.SerializeToMarkdown(document); Assert.Equal(@"\*literal\* \[value\]", serialized); } @@ -110,83 +110,20 @@ public void PreserveLiteralMarkdown_KeepsTypedMarkdownSyntax() FlowDocument document = new(); document.Blocks.Add(new Paragraph(new Run("**bold** [link](https://example.com)"))); - string serialized = MarkdownDocumentUtilities.SerializeToMarkdown(document, preserveLiteralMarkdown: true); + string serialized = MarkdownFlowDocumentUtilities.SerializeToMarkdown(document, preserveLiteralMarkdown: true); Assert.Equal("**bold** [link](https://example.com)", serialized); } - [Theory] - [InlineData("#")] - [InlineData("##")] - [InlineData(">")] - [InlineData(" >")] - [InlineData("-")] - [InlineData("1.")] - public void LiveBlockTriggerMarkers_AreRecognized(string marker) - { - Assert.True(MarkdownDocumentUtilities.ShouldPromoteLiveBlock(marker)); - } - - [Theory] - [InlineData("text")] - [InlineData("hello # world")] - [InlineData("1.2")] - public void NonTriggerText_DoesNotPromoteLiveBlock(string text) - { - Assert.False(MarkdownDocumentUtilities.ShouldPromoteLiveBlock(text)); - } - - [Theory] - [InlineData("**bold**")] - [InlineData("`code`")] - [InlineData("[link](https://example.com)")] - [InlineData("[ ] task")] - [InlineData("[x] done")] - public void CompletedMarkdownSyntax_PromotesLiveParsing(string text) - { - Assert.True(MarkdownDocumentUtilities.ShouldPromoteLiveMarkdown(text)); - } - - [Theory] - [InlineData("*")] - [InlineData("[link]")] - [InlineData("plain text")] - [InlineData("2026.04 release notes")] - public void IncompleteMarkdownSyntax_DoesNotPromoteLiveParsing(string text) - { - Assert.False(MarkdownDocumentUtilities.ShouldPromoteLiveMarkdown(text)); - } - - [Theory] - [InlineData("# Heading")] - [InlineData("> quote")] - [InlineData("- item")] - [InlineData("1. item")] - [InlineData("[link](https://example.com)")] - [InlineData("```csharp\nConsole.WriteLine(\"hi\");\n```")] - public void MarkdownLikeText_IsDetectedForPasteParsing(string text) - { - Assert.True(MarkdownDocumentUtilities.LooksLikeMarkdown(text)); - } - - [Theory] - [InlineData("Just a normal sentence.")] - [InlineData("2026.04 release notes")] - [InlineData("email me at joe@example.com")] - public void PlainText_IsNotDetectedAsMarkdown(string text) - { - Assert.False(MarkdownDocumentUtilities.LooksLikeMarkdown(text)); - } - /// /// Mirrors exactly what EditTextWindow.SelectInEditor does with a Find & Replace match: /// map the raw start and raw start+length offsets to positions independently, then read the /// rendered text between them. /// - private static string MapAndSlice(FlowDocument document, MarkdownDocumentUtilities.MarkdownOffsetMap map, int rawStart, int length) + private static string MapAndSlice(FlowDocument document, MarkdownFlowDocumentUtilities.MarkdownOffsetMap map, int rawStart, int length) { - TextPointer start = MarkdownDocumentUtilities.MapRawOffsetToPosition(document, map, rawStart); - TextPointer end = MarkdownDocumentUtilities.MapRawOffsetToPosition(document, map, rawStart + length); + TextPointer start = MarkdownFlowDocumentUtilities.MapRawOffsetToPosition(document, map, rawStart); + TextPointer end = MarkdownFlowDocumentUtilities.MapRawOffsetToPosition(document, map, rawStart + length); return new TextRange(start, end).Text; } @@ -194,8 +131,8 @@ private static string MapAndSlice(FlowDocument document, MarkdownDocumentUtiliti public void MapRawOffsetToPosition_SkipsStrippedBoldMarkers() { const string markdown = "Plain **bold** text with a [link](https://example.com)."; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("bold", StringComparison.Ordinal); @@ -206,8 +143,8 @@ public void MapRawOffsetToPosition_SkipsStrippedBoldMarkers() public void MapRawOffsetToPosition_SkipsLinkBracketsAndUrl() { const string markdown = "Plain **bold** text with a [link](https://example.com)."; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("link", StringComparison.Ordinal); @@ -218,8 +155,8 @@ public void MapRawOffsetToPosition_SkipsLinkBracketsAndUrl() public void MapRawOffsetToPosition_SkipsHeadingHashPrefix() { const string markdown = "# My Heading Title"; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("Heading", StringComparison.Ordinal); @@ -230,8 +167,8 @@ public void MapRawOffsetToPosition_SkipsHeadingHashPrefix() public void MapRawOffsetToPosition_SkipsListMarkers() { const string markdown = "- first item\n- second item\n- third item"; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("third", StringComparison.Ordinal); @@ -254,8 +191,8 @@ Second paragraph also has some words in it. Third paragraph has the target word right here. """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("target", StringComparison.Ordinal); @@ -266,8 +203,8 @@ Third paragraph has the target word right here. public void MapRawOffsetToPosition_HandlesCodeSpanBackticks() { const string markdown = "Run `dotnet build` to compile the project."; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("dotnet", StringComparison.Ordinal); @@ -278,8 +215,8 @@ public void MapRawOffsetToPosition_HandlesCodeSpanBackticks() public void MapRawOffsetToPosition_HandlesBoldNestedInsideLinkText() { const string markdown = "See the [**important** notes](https://example.com) page."; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("important", StringComparison.Ordinal); @@ -298,8 +235,8 @@ public void MapRawOffsetToPosition_HandlesTextInsideTableCell() | --- | --- | | Alpha | fortytwo | """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("fortytwo", StringComparison.Ordinal); @@ -314,8 +251,8 @@ public void MapRawOffsetToPosition_HandlesTextInEarlierTableCellOnSameRow() | --- | --- | | Alpha | fortytwo | """; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("Alpha", StringComparison.Ordinal); @@ -326,8 +263,8 @@ public void MapRawOffsetToPosition_HandlesTextInEarlierTableCellOnSameRow() public void MapRawOffsetToPosition_MapsBothEndsOfAMatchToTheExactRenderedSubstring() { const string markdown = "Plain text with a target word right here."; - FlowDocument document = MarkdownDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); - MarkdownDocumentUtilities.MarkdownOffsetMap map = MarkdownDocumentUtilities.BuildOffsetMap(document); + FlowDocument document = MarkdownFlowDocumentUtilities.CreateFlowDocument(markdown, new FontFamily("Segoe UI"), 16); + MarkdownFlowDocumentUtilities.MarkdownOffsetMap map = MarkdownFlowDocumentUtilities.BuildOffsetMap(document); int rawIndex = markdown.IndexOf("target word", StringComparison.Ordinal); diff --git a/Text-Grab.Core/Text-Grab.Core.csproj b/Text-Grab.Core/Text-Grab.Core.csproj index 7a46fc0d..7ed96db7 100644 --- a/Text-Grab.Core/Text-Grab.Core.csproj +++ b/Text-Grab.Core/Text-Grab.Core.csproj @@ -13,6 +13,7 @@ + diff --git a/Text-Grab.Core/Utilities/MarkdownDocumentUtilities.cs b/Text-Grab.Core/Utilities/MarkdownDocumentUtilities.cs new file mode 100644 index 00000000..179f9e58 --- /dev/null +++ b/Text-Grab.Core/Utilities/MarkdownDocumentUtilities.cs @@ -0,0 +1,176 @@ +using Markdig; +using Markdig.Extensions.AutoIdentifiers; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Text_Grab.Utilities; + +public static partial class MarkdownDocumentUtilities +{ + private static readonly Regex LiveBlockTriggerRegex = LiveBlockTrigger(); + private static readonly Regex LiveInlinePromotionRegex = LiveInlinePromotion(); + private static readonly Regex MarkdownPatternRegex = MarkdownPattern(); + + internal static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder() + .UseAutoIdentifiers(AutoIdentifierOptions.GitHub) // Must be BEFORE UseAdvancedExtensions to override default + .UseAdvancedExtensions() + .UseYamlFrontMatter() + .UseEmojiAndSmiley(enableSmileys: false) + .Build(); + + public static bool ShouldPromoteLiveBlock(string? lineTextBeforeSpace) + { + if (string.IsNullOrWhiteSpace(lineTextBeforeSpace)) + return false; + + return LiveBlockTriggerRegex.IsMatch(lineTextBeforeSpace); + } + + public static bool LooksLikeMarkdown(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + return false; + + return MarkdownPatternRegex.IsMatch(text); + } + + public static bool ShouldPromoteLiveMarkdown(string? paragraphText) + { + if (string.IsNullOrWhiteSpace(paragraphText)) + return false; + + return LiveInlinePromotionRegex.IsMatch(NormalizeDocumentText(paragraphText)); + } + + internal static int GetOrderedListStart(ListBlock listBlock) + { + return listBlock.IsOrdered + && int.TryParse(listBlock.OrderedStart, out int startIndex) + && startIndex > 0 + ? startIndex + : 1; + } + + internal static string GetCodeBlockText(LeafBlock block) + { + return NormalizeDocumentText(block.Lines.ToString()); + } + + internal static string EscapeMarkdownText(string? text) + { + if (string.IsNullOrEmpty(text)) + return string.Empty; + + string escapedText = text + .Replace("\\", "\\\\", StringComparison.Ordinal) + .Replace("`", "\\`", StringComparison.Ordinal) + .Replace("*", "\\*", StringComparison.Ordinal) + .Replace("_", "\\_", StringComparison.Ordinal) + .Replace("[", "\\[", StringComparison.Ordinal) + .Replace("]", "\\]", StringComparison.Ordinal) + .Replace("|", "\\|", StringComparison.Ordinal); + + escapedText = Regex.Replace(escapedText, @"^(#{1,6}\s)", @"\$1", RegexOptions.Multiline); + escapedText = Regex.Replace(escapedText, @"^(\s*>+)", @"\$1", RegexOptions.Multiline); + escapedText = Regex.Replace(escapedText, @"^(\s*[-+]\s)", @"\$1", RegexOptions.Multiline); + escapedText = Regex.Replace(escapedText, @"^(\s*\d+\.\s)", @"\$1", RegexOptions.Multiline); + return escapedText; + } + + internal static string EscapeLinkDestination(string destination) + { + return destination.Replace(")", "\\)", StringComparison.Ordinal); + } + + internal static string ApplyQuotePrefix(string text, string quotePrefix) + { + if (string.IsNullOrEmpty(quotePrefix)) + return text; + + return string.Join( + Environment.NewLine, + NormalizeNewlines(text).Split('\n').Select(line => string.IsNullOrEmpty(line) + ? quotePrefix.TrimEnd() + : $"{quotePrefix}{line}")); + } + + internal static string GetQuotePrefix(int quoteDepth) + { + if (quoteDepth <= 0) + return string.Empty; + + StringBuilder builder = new(); + for (int i = 0; i < quoteDepth; i++) + builder.Append("> "); + + return builder.ToString(); + } + + internal static string NormalizeDocumentText(string? text) + { + if (string.IsNullOrEmpty(text)) + return string.Empty; + + return NormalizeNewlines(text).TrimEnd('\n'); + } + + internal static string NormalizeNewlines(string text) => text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'); + + /// + /// A code span's covers the whole backtick-delimited run (e.g. + /// `dotnet build`), but is just the inner text. Assumes + /// a symmetric fence (equal backtick count on both sides), which covers the vast majority of + /// real-world code spans; degrades to the fenced span if that assumption doesn't hold. + /// + internal static int GetCodeSpanContentRawStart(CodeInline codeInline) + { + int totalLength = codeInline.Span.End - codeInline.Span.Start + 1; + int contentLength = codeInline.Content.Length; + int fenceLength = Math.Max(0, (totalLength - contentLength) / 2); + return codeInline.Span.Start + fenceLength; + } + + /// + /// A 's Span is not always tight to its own Content — + /// e.g. inside a pipe table cell, Markdig's reported span includes the cell's padding + /// whitespace ("| Alpha |"'s content is "Alpha" but the span covers " Alpha "), + /// while ordinary paragraph text elsewhere has no such padding and the span is already exact. + /// Searches the reported span's own window for the literal content and returns its tight bounds; + /// falls back to the untrimmed span if the content can't be found there (should not normally happen). + /// + internal static (int Start, int End) ResolveContentSpan(string source, string content, int spanStart, int spanEndExclusive) + { + if (string.IsNullOrEmpty(content) || spanStart < 0 || spanEndExclusive > source.Length || spanEndExclusive <= spanStart) + return (spanStart, spanEndExclusive); + + int windowLength = spanEndExclusive - spanStart; + if (content.Length > windowLength) + return (spanStart, spanEndExclusive); + + int found = source.IndexOf(content, spanStart, windowLength, StringComparison.Ordinal); + return found < 0 ? (spanStart, spanEndExclusive) : (found, found + content.Length); + } + + internal static string GetSourceSlice(string source, MarkdownObject markdownObject) + { + if (markdownObject.Span.Start < 0 + || markdownObject.Span.End < markdownObject.Span.Start + || markdownObject.Span.End >= source.Length) + return string.Empty; + + return source.Substring(markdownObject.Span.Start, markdownObject.Span.End - markdownObject.Span.Start + 1); + } + + [GeneratedRegex(@"^\s{0,3}(#{1,6}|>+|[-+*]|\d+[.)])$", RegexOptions.Compiled)] + private static partial Regex LiveBlockTrigger(); + + [GeneratedRegex(@"(^|\s)\[( |x|X)\](\s|$)|(\*\*|__)(?=\S).+?\4|(?+\s|[-+*]\s|\d+[.)]\s|```|~~~|---\s*$|___\s*$|\*\*\*\s*$)|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|(^|\n)\|.+\|\s*$", RegexOptions.Multiline | RegexOptions.Compiled)] + private static partial Regex MarkdownPattern(); +} diff --git a/Text-Grab/Utilities/MarkdownDocumentUtilities.cs b/Text-Grab/Utilities/MarkdownFlowDocumentUtilities.cs similarity index 79% rename from Text-Grab/Utilities/MarkdownDocumentUtilities.cs rename to Text-Grab/Utilities/MarkdownFlowDocumentUtilities.cs index 2db413b1..994ca491 100644 --- a/Text-Grab/Utilities/MarkdownDocumentUtilities.cs +++ b/Text-Grab/Utilities/MarkdownFlowDocumentUtilities.cs @@ -1,5 +1,3 @@ -using Markdig; -using Markdig.Extensions.AutoIdentifiers; using Markdig.Extensions.TaskLists; using Markdig.Syntax; using Markdig.Syntax.Inlines; @@ -7,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Windows; using System.Windows.Documents; using System.Windows.Media; @@ -25,19 +22,17 @@ namespace Text_Grab.Utilities; -public static partial class MarkdownDocumentUtilities +/// +/// The FlowDocument-bound half of the markdown editor's document model. Split out of +/// (now in Text-Grab.Core) because everything here touches +/// / types, which cannot move to the +/// portable tier. The pure AST-walking/regex/string helpers this class calls +/// (, +/// , etc.) stayed on the original type name +/// and are exposed internal for this class to reach. +/// +public static class MarkdownFlowDocumentUtilities { - private static readonly Regex LiveBlockTriggerRegex = LiveBlockTrigger(); - private static readonly Regex LiveInlinePromotionRegex = LiveInlinePromotion(); - private static readonly Regex MarkdownPatternRegex = MarkdownPattern(); - - private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder() - .UseAutoIdentifiers(AutoIdentifierOptions.GitHub) // Must be BEFORE UseAdvancedExtensions to override default - .UseAdvancedExtensions() - .UseYamlFrontMatter() - .UseEmojiAndSmiley(enableSmileys: false) - .Build(); - private enum MarkdownBlockRole { None, @@ -74,7 +69,7 @@ public static FlowDocument CreateFlowDocument(string? markdownText, FontFamily f PagePadding = new Thickness(0) }; - MarkdownDocument markdownDocument = Markdown.Parse(safeMarkdown, MarkdownPipeline); + MarkdownDocument markdownDocument = Markdig.Markdown.Parse(safeMarkdown, MarkdownDocumentUtilities.MarkdownPipeline); foreach (MarkdigBlock block in markdownDocument) AppendBlock(document.Blocks, block, safeMarkdown, quoteDepth: 0); @@ -105,7 +100,7 @@ public static string SerializeToMarkdown(FlowDocument document, bool preserveLit public static string GetDocumentPlainText(FlowDocument document) { ArgumentNullException.ThrowIfNull(document); - return NormalizeDocumentText(new TextRange(document.ContentStart, document.ContentEnd).Text); + return MarkdownDocumentUtilities.NormalizeDocumentText(new TextRange(document.ContentStart, document.ContentEnd).Text); } /// @@ -242,10 +237,6 @@ private static TextPointer MapWithinBlock(IReadOnlyList r /// insertion positions scoped to that one paragraph. Bounded by the paragraph's own size rather /// than the whole document, and — unlike a document-wide walk — correct even inside a table cell. /// - /// - /// Resolves a plain-text offset local to to an actual - /// by walking insertion positions scoped to that one paragraph. - /// /// /// A list item's non-navigable bullet/number marker is a known, narrow exception: WPF's /// counts it as part of the paragraph's rendered text (so the @@ -335,30 +326,6 @@ private static void CollectOffsetMappings(WpfInline inline, Paragraph owningPara } } - public static bool ShouldPromoteLiveBlock(string? lineTextBeforeSpace) - { - if (string.IsNullOrWhiteSpace(lineTextBeforeSpace)) - return false; - - return LiveBlockTriggerRegex.IsMatch(lineTextBeforeSpace); - } - - public static bool LooksLikeMarkdown(string? text) - { - if (string.IsNullOrWhiteSpace(text)) - return false; - - return MarkdownPatternRegex.IsMatch(text); - } - - public static bool ShouldPromoteLiveMarkdown(string? paragraphText) - { - if (string.IsNullOrWhiteSpace(paragraphText)) - return false; - - return LiveInlinePromotionRegex.IsMatch(NormalizeDocumentText(paragraphText)); - } - public static void ApplyTheme(FlowDocument document, FrameworkElement resourceHost, bool isLightTheme) { ArgumentNullException.ThrowIfNull(document); @@ -412,7 +379,7 @@ private static void AppendBlock(BlockCollection blocks, MarkdigBlock block, stri { MarkerStyle = listBlock.IsOrdered ? TextMarkerStyle.Decimal : TextMarkerStyle.Disc, Margin = new Thickness(0, 4, 0, 4), - StartIndex = GetOrderedListStart(listBlock), + StartIndex = MarkdownDocumentUtilities.GetOrderedListStart(listBlock), }; SetQuoteDepth(list, quoteDepth); @@ -432,11 +399,11 @@ private static void AppendBlock(BlockCollection blocks, MarkdigBlock block, stri break; case FencedCodeBlock fencedCodeBlock: - blocks.Add(CreateCodeParagraph(GetCodeBlockText(fencedCodeBlock), fencedCodeBlock.Info, quoteDepth, fencedCodeBlock.Span.Start, fencedCodeBlock.Span.End + 1)); + blocks.Add(CreateCodeParagraph(MarkdownDocumentUtilities.GetCodeBlockText(fencedCodeBlock), fencedCodeBlock.Info, quoteDepth, fencedCodeBlock.Span.Start, fencedCodeBlock.Span.End + 1)); break; case CodeBlock codeBlock: - blocks.Add(CreateCodeParagraph(GetCodeBlockText(codeBlock), info: null, quoteDepth, codeBlock.Span.Start, codeBlock.Span.End + 1)); + blocks.Add(CreateCodeParagraph(MarkdownDocumentUtilities.GetCodeBlockText(codeBlock), info: null, quoteDepth, codeBlock.Span.Start, codeBlock.Span.End + 1)); break; case ThematicBreakBlock thematicBreakBlock: @@ -458,7 +425,7 @@ private static void AppendBlock(BlockCollection blocks, MarkdigBlock block, stri break; default: - blocks.Add(CreateLiteralParagraph(GetSourceSlice(source, block), quoteDepth, block.Span.Start, block.Span.End + 1)); + blocks.Add(CreateLiteralParagraph(MarkdownDocumentUtilities.GetSourceSlice(source, block), quoteDepth, block.Span.Start, block.Span.End + 1)); break; } } @@ -555,7 +522,7 @@ private static void AppendInline(InlineCollection inlines, MarkdigInline inline, case LiteralInline literalInline: string literalContent = literalInline.Content.ToString(); Run contentRun = new(literalContent); - (int literalRawStart, int literalRawEnd) = ResolveContentSpan( + (int literalRawStart, int literalRawEnd) = MarkdownDocumentUtilities.ResolveContentSpan( source, literalContent, literalInline.Span.Start, literalInline.Span.End + 1); SetRawSpan(contentRun, literalRawStart, literalRawEnd); inlines.Add(contentRun); @@ -574,7 +541,7 @@ private static void AppendInline(InlineCollection inlines, MarkdigInline inline, // codeInline.Span covers the backtick fence too (e.g. "`dotnet build`"), but // Content is just the inner text ("dotnet build") — tag the content's own raw // range, not the fenced span, so this maps 1:1 instead of proportionally. - int codeContentRawStart = GetCodeSpanContentRawStart(codeInline); + int codeContentRawStart = MarkdownDocumentUtilities.GetCodeSpanContentRawStart(codeInline); SetRawSpan(codeRun, codeContentRawStart, codeContentRawStart + codeInline.Content.Length); inlines.Add(codeRun); break; @@ -620,7 +587,7 @@ private static void AppendInline(InlineCollection inlines, MarkdigInline inline, break; case LinkInline linkInline: - Run literalImageRun = new(GetSourceSlice(source, linkInline)); + Run literalImageRun = new(MarkdownDocumentUtilities.GetSourceSlice(source, linkInline)); SetInlineRole(literalImageRun, MarkdownInlineRole.LiteralMarkdown); SetRawSpan(literalImageRun, linkInline.Span.Start, linkInline.Span.End + 1); inlines.Add(literalImageRun); @@ -640,7 +607,7 @@ private static void AppendInline(InlineCollection inlines, MarkdigInline inline, break; default: - Run literalRun = new(GetSourceSlice(source, inline)); + Run literalRun = new(MarkdownDocumentUtilities.GetSourceSlice(source, inline)); SetInlineRole(literalRun, MarkdownInlineRole.LiteralMarkdown); SetRawSpan(literalRun, inline.Span.Start, inline.Span.End + 1); inlines.Add(literalRun); @@ -672,22 +639,22 @@ private static void WriteBlock(StringBuilder builder, WpfBlock block, int listDe private static void WriteParagraph(StringBuilder builder, Paragraph paragraph, bool preserveLiteralMarkdown) { - string quotePrefix = GetQuotePrefix(GetQuoteDepth(paragraph)); + string quotePrefix = MarkdownDocumentUtilities.GetQuotePrefix(GetQuoteDepth(paragraph)); if (GetBlockRole(paragraph) == MarkdownBlockRole.ThematicBreak) { - builder.Append(ApplyQuotePrefix("---", quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix("---", quotePrefix)); return; } if (GetBlockRole(paragraph) == MarkdownBlockRole.CodeBlock) { string codeInfo = GetCodeFenceInfo(paragraph); - string codeText = NormalizeDocumentText(new TextRange(paragraph.ContentStart, paragraph.ContentEnd).Text); + string codeText = MarkdownDocumentUtilities.NormalizeDocumentText(new TextRange(paragraph.ContentStart, paragraph.ContentEnd).Text); string fencedBlock = string.IsNullOrWhiteSpace(codeInfo) ? $"```{Environment.NewLine}{codeText}{Environment.NewLine}```" : $"```{codeInfo}{Environment.NewLine}{codeText}{Environment.NewLine}```"; - builder.Append(ApplyQuotePrefix(fencedBlock, quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix(fencedBlock, quotePrefix)); return; } @@ -696,12 +663,12 @@ private static void WriteParagraph(StringBuilder builder, Paragraph paragraph, b if (headingLevel > 0) content = $"{new string('#', headingLevel)} {content}"; - builder.Append(ApplyQuotePrefix(content, quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix(content, quotePrefix)); } private static void WriteList(StringBuilder builder, WpfList list, int listDepth, bool preserveLiteralMarkdown) { - string quotePrefix = GetQuotePrefix(GetQuoteDepth(list)); + string quotePrefix = MarkdownDocumentUtilities.GetQuotePrefix(GetQuoteDepth(list)); bool isOrdered = list.MarkerStyle == TextMarkerStyle.Decimal; int itemIndex = isOrdered ? Math.Max(1, list.StartIndex) : 1; bool isFirstItem = true; @@ -723,34 +690,25 @@ private static void WriteList(StringBuilder builder, WpfList list, int listDepth wroteItemBlock = true; } - string[] itemLines = NormalizeNewlines(itemBuilder.ToString()).Split('\n'); + string[] itemLines = MarkdownDocumentUtilities.NormalizeNewlines(itemBuilder.ToString()).Split('\n'); string indent = new(' ', listDepth * 2); string marker = isOrdered ? $"{itemIndex}. " : "- "; - builder.Append(ApplyQuotePrefix($"{indent}{marker}{itemLines[0]}", quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix($"{indent}{marker}{itemLines[0]}", quotePrefix)); string continuationIndent = $"{indent}{new string(' ', marker.Length)}"; for (int lineIndex = 1; lineIndex < itemLines.Length; lineIndex++) { builder.AppendLine(); - builder.Append(ApplyQuotePrefix($"{continuationIndent}{itemLines[lineIndex]}", quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix($"{continuationIndent}{itemLines[lineIndex]}", quotePrefix)); } itemIndex++; } } - private static int GetOrderedListStart(ListBlock listBlock) - { - return listBlock.IsOrdered - && int.TryParse(listBlock.OrderedStart, out int startIndex) - && startIndex > 0 - ? startIndex - : 1; - } - private static void WriteTable(StringBuilder builder, WpfTable table) { - string quotePrefix = GetQuotePrefix(GetQuoteDepth(table)); + string quotePrefix = MarkdownDocumentUtilities.GetQuotePrefix(GetQuoteDepth(table)); TableRowGroup? firstGroup = table.RowGroups.FirstOrDefault(); if (firstGroup is null || firstGroup.Rows.Count == 0) return; @@ -758,9 +716,9 @@ private static void WriteTable(StringBuilder builder, WpfTable table) List rows = [.. firstGroup.Rows.Cast()]; List headerCells = [.. rows[0].Cells.Cast().Select(SerializeTableCell)]; - builder.Append(ApplyQuotePrefix($"| {string.Join(" | ", headerCells)} |", quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix($"| {string.Join(" | ", headerCells)} |", quotePrefix)); builder.AppendLine(); - builder.Append(ApplyQuotePrefix($"| {string.Join(" | ", Enumerable.Repeat("---", Math.Max(1, headerCells.Count)))} |", quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix($"| {string.Join(" | ", Enumerable.Repeat("---", Math.Max(1, headerCells.Count)))} |", quotePrefix)); IEnumerable dataRows = rows.Count > 1 && rows[0].Cells.Cast().Any(GetIsTableHeader) ? rows.Skip(1) @@ -770,13 +728,13 @@ private static void WriteTable(StringBuilder builder, WpfTable table) { builder.AppendLine(); List rowCells = [.. row.Cells.Cast().Select(SerializeTableCell)]; - builder.Append(ApplyQuotePrefix($"| {string.Join(" | ", rowCells)} |", quotePrefix)); + builder.Append(MarkdownDocumentUtilities.ApplyQuotePrefix($"| {string.Join(" | ", rowCells)} |", quotePrefix)); } } private static string SerializeTableCell(WpfTableCell cell) { - string rawText = NormalizeDocumentText(new TextRange(cell.ContentStart, cell.ContentEnd).Text); + string rawText = MarkdownDocumentUtilities.NormalizeDocumentText(new TextRange(cell.ContentStart, cell.ContentEnd).Text); return rawText .Replace("|", "\\|", StringComparison.Ordinal) .Replace("\n", "
", StringComparison.Ordinal); @@ -803,17 +761,17 @@ private static void WriteInline(StringBuilder builder, WpfInline inline, bool pr builder.Append(GetInlineRole(run) switch { MarkdownInlineRole.TaskListMarker => GetTaskListMarkerChecked(run) ? "[x]" : "[ ]", - MarkdownInlineRole.CodeSpan => $"`{NormalizeDocumentText(run.Text)}`", + MarkdownInlineRole.CodeSpan => $"`{MarkdownDocumentUtilities.NormalizeDocumentText(run.Text)}`", MarkdownInlineRole.LiteralMarkdown => run.Text, _ when preserveLiteralMarkdown => run.Text, - _ => EscapeMarkdownText(run.Text) + _ => MarkdownDocumentUtilities.EscapeMarkdownText(run.Text) }); break; case Hyperlink hyperlink: string linkText = SerializeInlines(hyperlink.Inlines, preserveLiteralMarkdown); string linkTarget = hyperlink.NavigateUri?.OriginalString ?? linkText; - builder.Append($"[{linkText}]({EscapeLinkDestination(linkTarget)})"); + builder.Append($"[{linkText}]({MarkdownDocumentUtilities.EscapeLinkDestination(linkTarget)})"); break; case Bold bold: @@ -830,7 +788,7 @@ private static void WriteInline(StringBuilder builder, WpfInline inline, bool pr case Span span when GetInlineRole(span) == MarkdownInlineRole.CodeSpan: builder.Append('`'); - builder.Append(NormalizeDocumentText(new TextRange(span.ContentStart, span.ContentEnd).Text)); + builder.Append(MarkdownDocumentUtilities.NormalizeDocumentText(new TextRange(span.ContentStart, span.ContentEnd).Text)); builder.Append('`'); break; @@ -981,148 +939,38 @@ private static Brush FindBrush(FrameworkElement resourceHost, string resourceKey }; } - private static string GetCodeBlockText(LeafBlock block) - { - return NormalizeDocumentText(block.Lines.ToString()); - } - private static string SerializeLiteralText(TextElement element, bool preserveLiteralMarkdown) { - string text = NormalizeDocumentText(new TextRange(element.ContentStart, element.ContentEnd).Text); - return preserveLiteralMarkdown ? text : EscapeMarkdownText(text); - } - - private static string EscapeMarkdownText(string? text) - { - if (string.IsNullOrEmpty(text)) - return string.Empty; - - string escapedText = text - .Replace("\\", "\\\\", StringComparison.Ordinal) - .Replace("`", "\\`", StringComparison.Ordinal) - .Replace("*", "\\*", StringComparison.Ordinal) - .Replace("_", "\\_", StringComparison.Ordinal) - .Replace("[", "\\[", StringComparison.Ordinal) - .Replace("]", "\\]", StringComparison.Ordinal) - .Replace("|", "\\|", StringComparison.Ordinal); - - escapedText = Regex.Replace(escapedText, @"^(#{1,6}\s)", @"\$1", RegexOptions.Multiline); - escapedText = Regex.Replace(escapedText, @"^(\s*>+)", @"\$1", RegexOptions.Multiline); - escapedText = Regex.Replace(escapedText, @"^(\s*[-+]\s)", @"\$1", RegexOptions.Multiline); - escapedText = Regex.Replace(escapedText, @"^(\s*\d+\.\s)", @"\$1", RegexOptions.Multiline); - return escapedText; - } - - private static string EscapeLinkDestination(string destination) - { - return destination.Replace(")", "\\)", StringComparison.Ordinal); - } - - private static string ApplyQuotePrefix(string text, string quotePrefix) - { - if (string.IsNullOrEmpty(quotePrefix)) - return text; - - return string.Join( - Environment.NewLine, - NormalizeNewlines(text).Split('\n').Select(line => string.IsNullOrEmpty(line) - ? quotePrefix.TrimEnd() - : $"{quotePrefix}{line}")); - } - - private static string GetQuotePrefix(int quoteDepth) - { - if (quoteDepth <= 0) - return string.Empty; - - StringBuilder builder = new(); - for (int i = 0; i < quoteDepth; i++) - builder.Append("> "); - - return builder.ToString(); - } - - private static string NormalizeDocumentText(string? text) - { - if (string.IsNullOrEmpty(text)) - return string.Empty; - - return NormalizeNewlines(text).TrimEnd('\n'); - } - - private static string NormalizeNewlines(string text) => text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'); - - /// - /// A code span's covers the whole backtick-delimited run (e.g. - /// `dotnet build`), but is just the inner text. Assumes - /// a symmetric fence (equal backtick count on both sides), which covers the vast majority of - /// real-world code spans; degrades to the fenced span if that assumption doesn't hold. - /// - private static int GetCodeSpanContentRawStart(CodeInline codeInline) - { - int totalLength = codeInline.Span.End - codeInline.Span.Start + 1; - int contentLength = codeInline.Content.Length; - int fenceLength = Math.Max(0, (totalLength - contentLength) / 2); - return codeInline.Span.Start + fenceLength; - } - - /// - /// A 's Span is not always tight to its own Content — - /// e.g. inside a pipe table cell, Markdig's reported span includes the cell's padding - /// whitespace ("| Alpha |"'s content is "Alpha" but the span covers " Alpha "), - /// while ordinary paragraph text elsewhere has no such padding and the span is already exact. - /// Searches the reported span's own window for the literal content and returns its tight bounds; - /// falls back to the untrimmed span if the content can't be found there (should not normally happen). - /// - private static (int Start, int End) ResolveContentSpan(string source, string content, int spanStart, int spanEndExclusive) - { - if (string.IsNullOrEmpty(content) || spanStart < 0 || spanEndExclusive > source.Length || spanEndExclusive <= spanStart) - return (spanStart, spanEndExclusive); - - int windowLength = spanEndExclusive - spanStart; - if (content.Length > windowLength) - return (spanStart, spanEndExclusive); - - int found = source.IndexOf(content, spanStart, windowLength, StringComparison.Ordinal); - return found < 0 ? (spanStart, spanEndExclusive) : (found, found + content.Length); - } - - private static string GetSourceSlice(string source, MarkdownObject markdownObject) - { - if (markdownObject.Span.Start < 0 - || markdownObject.Span.End < markdownObject.Span.Start - || markdownObject.Span.End >= source.Length) - return string.Empty; - - return source.Substring(markdownObject.Span.Start, markdownObject.Span.End - markdownObject.Span.Start + 1); + string text = MarkdownDocumentUtilities.NormalizeDocumentText(new TextRange(element.ContentStart, element.ContentEnd).Text); + return preserveLiteralMarkdown ? text : MarkdownDocumentUtilities.EscapeMarkdownText(text); } private static readonly DependencyProperty QuoteDepthProperty = - DependencyProperty.RegisterAttached("QuoteDepth", typeof(int), typeof(MarkdownDocumentUtilities), new PropertyMetadata(0)); + DependencyProperty.RegisterAttached("QuoteDepth", typeof(int), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(0)); private static readonly DependencyProperty HeadingLevelProperty = - DependencyProperty.RegisterAttached("HeadingLevel", typeof(int), typeof(MarkdownDocumentUtilities), new PropertyMetadata(0)); + DependencyProperty.RegisterAttached("HeadingLevel", typeof(int), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(0)); private static readonly DependencyProperty BlockRoleProperty = - DependencyProperty.RegisterAttached("BlockRole", typeof(MarkdownBlockRole), typeof(MarkdownDocumentUtilities), new PropertyMetadata(MarkdownBlockRole.None)); + DependencyProperty.RegisterAttached("BlockRole", typeof(MarkdownBlockRole), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(MarkdownBlockRole.None)); private static readonly DependencyProperty InlineRoleProperty = - DependencyProperty.RegisterAttached("InlineRole", typeof(MarkdownInlineRole), typeof(MarkdownDocumentUtilities), new PropertyMetadata(MarkdownInlineRole.None)); + DependencyProperty.RegisterAttached("InlineRole", typeof(MarkdownInlineRole), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(MarkdownInlineRole.None)); private static readonly DependencyProperty TaskListMarkerCheckedProperty = - DependencyProperty.RegisterAttached("TaskListMarkerChecked", typeof(bool), typeof(MarkdownDocumentUtilities), new PropertyMetadata(false)); + DependencyProperty.RegisterAttached("TaskListMarkerChecked", typeof(bool), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(false)); private static readonly DependencyProperty CodeFenceInfoProperty = - DependencyProperty.RegisterAttached("CodeFenceInfo", typeof(string), typeof(MarkdownDocumentUtilities), new PropertyMetadata(string.Empty)); + DependencyProperty.RegisterAttached("CodeFenceInfo", typeof(string), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(string.Empty)); private static readonly DependencyProperty IsTableHeaderProperty = - DependencyProperty.RegisterAttached("IsTableHeader", typeof(bool), typeof(MarkdownDocumentUtilities), new PropertyMetadata(false)); + DependencyProperty.RegisterAttached("IsTableHeader", typeof(bool), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(false)); private static readonly DependencyProperty RawSpanStartProperty = - DependencyProperty.RegisterAttached("RawSpanStart", typeof(int), typeof(MarkdownDocumentUtilities), new PropertyMetadata(-1)); + DependencyProperty.RegisterAttached("RawSpanStart", typeof(int), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(-1)); private static readonly DependencyProperty RawSpanEndProperty = - DependencyProperty.RegisterAttached("RawSpanEnd", typeof(int), typeof(MarkdownDocumentUtilities), new PropertyMetadata(-1)); + DependencyProperty.RegisterAttached("RawSpanEnd", typeof(int), typeof(MarkdownFlowDocumentUtilities), new PropertyMetadata(-1)); /// /// Records the [start, end) range in the raw markdown source that a Run's rendered text was @@ -1155,14 +1003,4 @@ private static void SetRawSpan(DependencyObject element, int start, int endExclu private static string GetCodeFenceInfo(DependencyObject element) => (string)element.GetValue(CodeFenceInfoProperty); private static void SetIsTableHeader(DependencyObject element, bool value) => element.SetValue(IsTableHeaderProperty, value); private static bool GetIsTableHeader(DependencyObject element) => (bool)element.GetValue(IsTableHeaderProperty); - - - [GeneratedRegex(@"^\s{0,3}(#{1,6}|>+|[-+*]|\d+[.)])$", RegexOptions.Compiled)] - private static partial Regex LiveBlockTrigger(); - - [GeneratedRegex(@"(^|\s)\[( |x|X)\](\s|$)|(\*\*|__)(?=\S).+?\4|(?+\s|[-+*]\s|\d+[.)]\s|```|~~~|---\s*$|___\s*$|\*\*\*\s*$)|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|(^|\n)\|.+\|\s*$", RegexOptions.Multiline | RegexOptions.Compiled)] - private static partial Regex MarkdownPattern(); } diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index b793b8aa..6c97b042 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -124,7 +124,7 @@ public partial class EditTextWindow : Wpf.Ui.Controls.FluentWindow private bool isSyncingTextFromMarkdown = false; private bool isApplyingSpreadsheetLayout = false; private bool isApplyingMarkdownDocument = false; - private MarkdownDocumentUtilities.MarkdownOffsetMap markdownOffsetMap = new([], []); + private MarkdownFlowDocumentUtilities.MarkdownOffsetMap markdownOffsetMap = new([], []); private string? markdownOffsetMapSourceText; private bool isLoadingOpenedFile = false; private bool hasPendingFileEdits = false; @@ -1005,11 +1005,11 @@ private void RefreshMarkdownFromText() private void LoadMarkdownDocumentFromText(string? markdownText) { isApplyingMarkdownDocument = true; - MarkdownEditorControl.Document = MarkdownDocumentUtilities.CreateFlowDocument( + MarkdownEditorControl.Document = MarkdownFlowDocumentUtilities.CreateFlowDocument( markdownText, MarkdownEditorControl.FontFamily, MarkdownEditorControl.FontSize); - markdownOffsetMap = MarkdownDocumentUtilities.BuildOffsetMap(MarkdownEditorControl.Document); + markdownOffsetMap = MarkdownFlowDocumentUtilities.BuildOffsetMap(MarkdownEditorControl.Document); markdownOffsetMapSourceText = markdownText ?? string.Empty; ApplyMarkdownTheme(); ApplyMarkdownWrapSetting(); @@ -1023,7 +1023,7 @@ private void SyncMarkdownTextFromDocument() return; isSyncingTextFromMarkdown = true; - PassedTextControl.Text = MarkdownDocumentUtilities.SerializeToMarkdown( + PassedTextControl.Text = MarkdownFlowDocumentUtilities.SerializeToMarkdown( MarkdownEditorControl.Document, preserveLiteralMarkdown: true); isSyncingTextFromMarkdown = false; @@ -1034,7 +1034,7 @@ private void ApplyMarkdownTheme() if (MarkdownEditorControl.Document is null) return; - MarkdownDocumentUtilities.ApplyTheme( + MarkdownFlowDocumentUtilities.ApplyTheme( MarkdownEditorControl.Document, this, SystemThemeUtility.IsLightTheme()); @@ -3292,7 +3292,7 @@ internal IEnumerable GetSelectedOrAllTextSegmentsForPreview() /// markdown / are measured against — bold /// markers, heading #s, list bullets, link brackets, etc. are stripped on render. The /// index is translated through the offset map built alongside the rendered document - /// () rather than applied directly. + /// () rather than applied directly. /// public void SelectInEditor(int index, int length) { @@ -3304,8 +3304,8 @@ public void SelectInEditor(int index, int length) if (MarkdownEditorControl.Document is not null) { - TextPointer start = MarkdownDocumentUtilities.MapRawOffsetToPosition(MarkdownEditorControl.Document, markdownOffsetMap, index); - TextPointer end = MarkdownDocumentUtilities.MapRawOffsetToPosition(MarkdownEditorControl.Document, markdownOffsetMap, index + length); + TextPointer start = MarkdownFlowDocumentUtilities.MapRawOffsetToPosition(MarkdownEditorControl.Document, markdownOffsetMap, index); + TextPointer end = MarkdownFlowDocumentUtilities.MapRawOffsetToPosition(MarkdownEditorControl.Document, markdownOffsetMap, index + length); MarkdownEditorControl.Selection.Select(start, end); MarkdownEditorControl.Focus(); start.Paragraph?.BringIntoView(); @@ -4715,8 +4715,8 @@ private void MarkdownEditorControl_Pasting(object sender, DataObjectPastingEvent bool shouldParseAsMarkdown = MarkdownDocumentUtilities.LooksLikeMarkdown(pastedText); int selectionStartOffset = GetMarkdownPlainTextOffset(MarkdownEditorControl.Selection.Start); int renderedPasteLength = shouldParseAsMarkdown - ? MarkdownDocumentUtilities.GetDocumentPlainText( - MarkdownDocumentUtilities.CreateFlowDocument( + ? MarkdownFlowDocumentUtilities.GetDocumentPlainText( + MarkdownFlowDocumentUtilities.CreateFlowDocument( pastedText, MarkdownEditorControl.FontFamily, MarkdownEditorControl.FontSize)).Length @@ -5740,7 +5740,7 @@ private void UpdateLineAndColumnText() string plainText = MarkdownEditorControl.Document is null ? string.Empty - : MarkdownDocumentUtilities.GetDocumentPlainText(MarkdownEditorControl.Document); + : MarkdownFlowDocumentUtilities.GetDocumentPlainText(MarkdownEditorControl.Document); string selectedText = MarkdownEditorControl.Selection.Text.TrimEnd('\r', '\n'); BottomBarText.Text = string.IsNullOrEmpty(selectedText) From 7e539c06e7edeece60c573f1af2339b51959dd78 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:55:18 -0500 Subject: [PATCH 21/54] Move eight zero-blocker files into Text-Grab.Core.Windows Batch 3a. Verified each file's actual type usage against invariant 8 rather than trusting the plan's list: - NativeMethods.cs, OSInterop.cs (both file-scoped, no namespace) - pure P/Invoke over user32/gdi32/shcore/shell32/kernel32/dwmapi, System and System.Runtime.InteropServices only. - OSInterop.cs's GetAsyncKeyState(System.Windows.Forms.Keys) overload (line 125) was the file's only System.Windows.Forms reference in 1292 lines, confirmed zero call sites (WindowUtilities.cs:289 uses the int overload). Deleted as part of the move, per plan section 8. - RegistryMonitor.cs (namespace RegistryUtils, vendored - kept exactly) - pure P/Invoke over advapi32 plus Microsoft.Win32.Registry types. - DesktopNotificationManagerCompat.cs (namespace Text_Grab) - COM activation and toast notification plumbing over Microsoft.Win32, Windows.UI.Notifications (WinRT) and P/Invoke; no WPF types. - Models/GeneratedOcrLinesWords.cs - uses Windows.Foundation.Rect (WinRT), not System.Windows.Rect; confirmed the distinction invariant 8 calls out. IOcrLinesWords/IOcrLine/IOcrWord it implements already live in Core.Windows. - Models/UiAutomationLang.cs, WindowsAiLang.cs, WindowsAiDescriptionLang.cs - plain ILanguage implementations (Text_Grab.Interfaces.ILanguage, already in Core.Windows) over Windows.Globalization only. All eight kept their original namespace and moved via git mv with no call-site changes needed - every consumer is elsewhere in the app, which reaches these types transitively through the existing project reference, and Core.Windows already has InternalsVisibleTo("Text-Grab") for the two internal P/Invoke classes. One project setting had to move: NativeMethods.cs and OSInterop.cs use LibraryImportAttribute-generated marshalling stubs, which require AllowUnsafeBlocks. Added it to Text-Grab.Core.Windows.csproj (already set in Text-Grab.csproj, so no behavior change for the app). No files deferred from this batch's list. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp --- .../DesktopNotificationManagerCompat.cs | 0 .../Models/GeneratedOcrLinesWords.cs | 0 .../Models/UiAutomationLang.cs | 0 .../Models/WindowsAiDescriptionLang.cs | 0 {Text-Grab => Text-Grab.Core.Windows}/Models/WindowsAiLang.cs | 0 {Text-Grab => Text-Grab.Core.Windows}/NativeMethods.cs | 0 {Text-Grab => Text-Grab.Core.Windows}/OSInterop.cs | 3 --- Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj | 1 + .../Utilities/RegistryMonitor.cs | 0 9 files changed, 1 insertion(+), 3 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/DesktopNotificationManagerCompat.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/GeneratedOcrLinesWords.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/UiAutomationLang.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/WindowsAiDescriptionLang.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/WindowsAiLang.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/NativeMethods.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/OSInterop.cs (99%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/RegistryMonitor.cs (100%) diff --git a/Text-Grab/DesktopNotificationManagerCompat.cs b/Text-Grab.Core.Windows/DesktopNotificationManagerCompat.cs similarity index 100% rename from Text-Grab/DesktopNotificationManagerCompat.cs rename to Text-Grab.Core.Windows/DesktopNotificationManagerCompat.cs diff --git a/Text-Grab/Models/GeneratedOcrLinesWords.cs b/Text-Grab.Core.Windows/Models/GeneratedOcrLinesWords.cs similarity index 100% rename from Text-Grab/Models/GeneratedOcrLinesWords.cs rename to Text-Grab.Core.Windows/Models/GeneratedOcrLinesWords.cs diff --git a/Text-Grab/Models/UiAutomationLang.cs b/Text-Grab.Core.Windows/Models/UiAutomationLang.cs similarity index 100% rename from Text-Grab/Models/UiAutomationLang.cs rename to Text-Grab.Core.Windows/Models/UiAutomationLang.cs diff --git a/Text-Grab/Models/WindowsAiDescriptionLang.cs b/Text-Grab.Core.Windows/Models/WindowsAiDescriptionLang.cs similarity index 100% rename from Text-Grab/Models/WindowsAiDescriptionLang.cs rename to Text-Grab.Core.Windows/Models/WindowsAiDescriptionLang.cs diff --git a/Text-Grab/Models/WindowsAiLang.cs b/Text-Grab.Core.Windows/Models/WindowsAiLang.cs similarity index 100% rename from Text-Grab/Models/WindowsAiLang.cs rename to Text-Grab.Core.Windows/Models/WindowsAiLang.cs diff --git a/Text-Grab/NativeMethods.cs b/Text-Grab.Core.Windows/NativeMethods.cs similarity index 100% rename from Text-Grab/NativeMethods.cs rename to Text-Grab.Core.Windows/NativeMethods.cs diff --git a/Text-Grab/OSInterop.cs b/Text-Grab.Core.Windows/OSInterop.cs similarity index 99% rename from Text-Grab/OSInterop.cs rename to Text-Grab.Core.Windows/OSInterop.cs index 578aa536..e4b990d7 100644 --- a/Text-Grab/OSInterop.cs +++ b/Text-Grab.Core.Windows/OSInterop.cs @@ -121,9 +121,6 @@ public class MONITORINFOEX public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); - [LibraryImport("user32.dll")] - public static partial short GetAsyncKeyState(System.Windows.Forms.Keys vKey); - [LibraryImport("user32.dll")] public static partial uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index 5e48e510..b252eed7 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -6,6 +6,7 @@ Text_Grab enable enable + true false false false diff --git a/Text-Grab/Utilities/RegistryMonitor.cs b/Text-Grab.Core.Windows/Utilities/RegistryMonitor.cs similarity index 100% rename from Text-Grab/Utilities/RegistryMonitor.cs rename to Text-Grab.Core.Windows/Utilities/RegistryMonitor.cs From 966a49720ef00271e7592b1b2ee8bd27e8e08121 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 07:59:12 -0500 Subject: [PATCH 22/54] Move SettingsStorageExtensions and LimitedAccessFeatureUtilities into Text-Grab.Core.Windows Batch 3b, unblocked by Wave 1's Json.cs landing in Core. - Extensions/SettingsStorageExtensions.cs (namespace Text_Grab.Helpers, kept exactly) - WinRT Windows.Storage/Windows.Storage.Streams helpers for ApplicationData/StorageFolder/ApplicationDataContainer, calling Json.StringifyAsync/ToObjectAsync from the same namespace (Core's Utilities/Json.cs, batch 1a). No other type dependencies. - Utilities/LimitedAccessFeatureUtilities.cs (namespace Text_Grab.Utilities, kept exactly) - the first link in the Windows AI chain 3c continues. Pure Windows.ApplicationModel (LimitedAccessFeatures, Package.Current) plus reflection over its own assembly's AssemblyMetadataAttribute. Stayed internal; Core.Windows already has InternalsVisibleTo("Text-Grab") so its one remaining app-side caller, WinAiLanguageModel.cs, is unaffected until 3c moves it too. LimitedAccessFeatureUtilities.GetSetting reads typeof(LimitedAccessFeatureUtilities).Assembly's AssemblyMetadata, which would have silently started reading the wrong (now-empty) assembly's metadata once the type moved - the LafToken/LafPublisherId MSBuild wiring that populates it was only in Text-Grab.csproj. Moved that PropertyGroup/ItemGroup pair to Text-Grab.Core.Windows.csproj instead of leaving it behind: Text-Grab.csproj's ProjectReference to Core.Windows carries the same global MSBuild properties into its build by default, so `dotnet build -p:LafToken=... -p:LafPublisherId=...` against Text-Grab.csproj, the wapproj, or CI's publish steps all still reach the new home unchanged. Verified end-to-end: built with -p:LafToken=TESTTOKEN -p:LafPublisherId=TESTPUB and confirmed both AssemblyMetadata attributes land on Text-Grab.Core.Windows.dll via its generated AssemblyInfo.cs. Updated the doc comment in LimitedAccessFeatureUtilities.cs and docs/Configuring-LAF-Environment-Variables.md to match. No files deferred from this batch's list. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp --- .../Extensions/SettingsStorageExtensions.cs | 0 .../Text-Grab.Core.Windows.csproj | 26 +++++++++++++++++++ .../LimitedAccessFeatureUtilities.cs | 2 +- Text-Grab/Text-Grab.csproj | 23 +++++----------- docs/Configuring-LAF-Environment-Variables.md | 8 +++--- 5 files changed, 38 insertions(+), 21 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Extensions/SettingsStorageExtensions.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/LimitedAccessFeatureUtilities.cs (99%) diff --git a/Text-Grab/Extensions/SettingsStorageExtensions.cs b/Text-Grab.Core.Windows/Extensions/SettingsStorageExtensions.cs similarity index 100% rename from Text-Grab/Extensions/SettingsStorageExtensions.cs rename to Text-Grab.Core.Windows/Extensions/SettingsStorageExtensions.cs diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index b252eed7..334e86c4 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -17,6 +17,32 @@ + + + $(LAF_TOKEN) + $(LAF_PUBLISHER_ID) + + + + + + + + + + diff --git a/Text-Grab/Utilities/LimitedAccessFeatureUtilities.cs b/Text-Grab.Core.Windows/Utilities/LimitedAccessFeatureUtilities.cs similarity index 99% rename from Text-Grab/Utilities/LimitedAccessFeatureUtilities.cs rename to Text-Grab.Core.Windows/Utilities/LimitedAccessFeatureUtilities.cs index a1336980..fe74e25d 100644 --- a/Text-Grab/Utilities/LimitedAccessFeatureUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/LimitedAccessFeatureUtilities.cs @@ -17,7 +17,7 @@ namespace Text_Grab.Utilities; /// Tokens are requested from Microsoft at https://aka.ms/laffeatures and must not be committed to /// source control, so the token and publisher ID are read at runtime from (in order): /// 1. AssemblyMetadata baked in at build time — set the MSBuild properties -/// LafToken and LafPublisherId (see Text-Grab.csproj). +/// LafToken and LafPublisherId (see Text-Grab.Core.Windows.csproj). /// 2. The LAF_TOKEN and LAF_PUBLISHER_ID environment variables, for local development. /// /// This mirrors how microsoft/ai-dev-gallery handles the same feature. diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index 4ad22d04..0438fe4b 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -34,24 +34,13 @@ before translate / summarize / rewrite / text-to-table will run; request one at https://aka.ms/laffeatures. - Never commit a token. Pass it in at build time instead: - dotnet build -p:LafToken="..." -p:LafPublisherId="..." - or set the LAF_TOKEN / LAF_PUBLISHER_ID environment variables for local development, which - the properties below pick up so Visual Studio builds get the token too. See - docs/Configuring-LAF-Environment-Variables.md. + LimitedAccessFeatureUtilities reads this back via its own assembly's AssemblyMetadata, and it + lives in Text-Grab.Core.Windows, so the LafToken/LafPublisherId wiring is defined there (see + Text-Grab.Core.Windows.csproj) rather than here. Global MSBuild properties passed to this + project's build (dotnet build/publish -p:LafToken=... -p:LafPublisherId=...) flow through the + ProjectReference below to that project's build unchanged, so nothing else has to change here. + See docs/Configuring-LAF-Environment-Variables.md. --> - - $(LAF_TOKEN) - $(LAF_PUBLISHER_ID) - - - - - - - - - diff --git a/docs/Configuring-LAF-Environment-Variables.md b/docs/Configuring-LAF-Environment-Variables.md index 41608fc6..04d84c78 100644 --- a/docs/Configuring-LAF-Environment-Variables.md +++ b/docs/Configuring-LAF-Environment-Variables.md @@ -42,9 +42,11 @@ setx LAF_TOKEN "" setx LAF_PUBLISHER_ID "" ``` -`Text-Grab.csproj` defaults the `LafToken` / `LafPublisherId` MSBuild properties from these -environment variables, so ordinary `dotnet build` and Visual Studio builds bake the token in without -any extra flags. +`Text-Grab.Core.Windows.csproj` defaults the `LafToken` / `LafPublisherId` MSBuild properties from +these environment variables, so ordinary `dotnet build` and Visual Studio builds bake the token in +without any extra flags. Building `Text-Grab.csproj` (or the wapproj) still works the same way: its +`ProjectReference` to `Text-Grab.Core.Windows.csproj` carries the same global MSBuild properties +into that project's build. ## Explicit build-time injection From 4711c5e8e38612fa311b946f3ca80775e0ef6856 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 08:02:42 -0500 Subject: [PATCH 23/54] Move the Windows AI chain into Text-Grab.Core.Windows Batch 3c, continuing the WinAI chain that 3b started. - Utilities/WinAiLanguageModel.cs (namespace Text_Grab.Utilities, kept exactly) - the shared Phi Silica LanguageModel wrapper. Its three dependencies had all already landed: PackageIdentity (1b), OSInterop.IsWindows10() (3a), LimitedAccessFeatureUtilities (3b). Microsoft.WindowsAppSDK.AI was already referenced by Text-Grab.Core.Windows.csproj (added earlier for WinAiOcrLinesWords), so no package changes were needed. - Utilities/WinAiTranslator.cs, Utilities/WinAiMeetingNotes.cs (same namespace, kept exactly) - both needed only WinAiLanguageModel and, for the translator, the already-Core LanguageHeuristics. Moved unchanged. Self-assembly-reflection audit (the specific risk flagged after 3b's LimitedAccessFeatureUtilities near-miss): none of the three files read their own assembly. WinAiLanguageModel's one call into app-side state was AppUtilities.IsPackaged(), a plain forwarder to PackageIdentity.IsPackaged() (Core.Windows, same Text_Grab.Utilities namespace) - swapped to call PackageIdentity directly since AppUtilities itself stays in the app. WinAiTranslator and WinAiMeetingNotes call no assembly-scoped APIs at all. Both are internal static classes; Core.Windows's existing InternalsVisibleTo("Text-Grab") keeps every app call site (GrabFrame, EditTextWindow, and others) compiling unchanged. No package references moved - Microsoft.WindowsAppSDK.AI stays in Text-Grab.csproj too, since OcrUtilities.cs and WindowsAiUtilities.cs (both still app-side, both on the deferred ledger) use the same Microsoft.Windows.AI namespaces. No files deferred from this batch's list. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp --- .../Utilities/WinAiLanguageModel.cs | 2 +- .../Utilities/WinAiMeetingNotes.cs | 0 .../Utilities/WinAiTranslator.cs | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/WinAiLanguageModel.cs (99%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/WinAiMeetingNotes.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/WinAiTranslator.cs (100%) diff --git a/Text-Grab/Utilities/WinAiLanguageModel.cs b/Text-Grab.Core.Windows/Utilities/WinAiLanguageModel.cs similarity index 99% rename from Text-Grab/Utilities/WinAiLanguageModel.cs rename to Text-Grab.Core.Windows/Utilities/WinAiLanguageModel.cs index fe7118f5..0d67b84f 100644 --- a/Text-Grab/Utilities/WinAiLanguageModel.cs +++ b/Text-Grab.Core.Windows/Utilities/WinAiLanguageModel.cs @@ -90,7 +90,7 @@ internal static class WinAiLanguageModel /// internal static (bool Available, string? Reason) CheckAvailability() { - if (!AppUtilities.IsPackaged()) + if (!PackageIdentity.IsPackaged()) return (false, "Windows AI is only available when Text-Grab runs as an installed (packaged) app."); if (OSInterop.IsWindows10()) diff --git a/Text-Grab/Utilities/WinAiMeetingNotes.cs b/Text-Grab.Core.Windows/Utilities/WinAiMeetingNotes.cs similarity index 100% rename from Text-Grab/Utilities/WinAiMeetingNotes.cs rename to Text-Grab.Core.Windows/Utilities/WinAiMeetingNotes.cs diff --git a/Text-Grab/Utilities/WinAiTranslator.cs b/Text-Grab.Core.Windows/Utilities/WinAiTranslator.cs similarity index 100% rename from Text-Grab/Utilities/WinAiTranslator.cs rename to Text-Grab.Core.Windows/Utilities/WinAiTranslator.cs From dff9aa44f0bced893452c46fff391d4618a36797 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 08:04:53 -0500 Subject: [PATCH 24/54] Split Extensions/LanguageExtensions.cs into Core.Windows and an app-side RTL facade Batch 3d. XmlLanguage comes from PresentationCore (WPF), so it cannot cross into Text-Grab.Core.Windows, and reconnaissance confirmed the RTL path is reachable in production from GrabFrame, EditTextWindow and OcrUtilities - not a dead using. - Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs (namespace Text_Grab, class name kept exactly) - IsSpaceJoining (both the Windows.Globalization.Language and ILanguage overloads), IsLatinBased, AsLanguage, AsILanguage. Keeping the original type name means every existing call site in the app (OcrUtilities, GrabFrame, HistoryInfo, LanguageService, PdfDocumentRenderer, ...) resolves unchanged through the existing project reference - no call-site edits were needed at all, confirmed by a clean Tests.csproj build. - Text-Grab/Extensions/LanguageRtlExtensions.cs (new file, same Text_Grab namespace, new type name) - IsRightToLeft(this Language) (the XmlLanguage lookup) and IsRightToLeft(this ILanguage) (the GlobalLang branch calls the Language overload, so the two stay together). Named distinctly rather than reusing LanguageExtensions: since both classes end up in the same Text_Grab namespace once the app references Core.Windows, a same-named extension method in both would have been a CS0121 ambiguity at every call site instead of one clean split. No package references or settings touched. No files deferred from this batch's list. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp --- .../Extensions/LanguageExtensions.cs | 18 ---------- Text-Grab/Extensions/LanguageRtlExtensions.cs | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 18 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Extensions/LanguageExtensions.cs (70%) create mode 100644 Text-Grab/Extensions/LanguageRtlExtensions.cs diff --git a/Text-Grab/Extensions/LanguageExtensions.cs b/Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs similarity index 70% rename from Text-Grab/Extensions/LanguageExtensions.cs rename to Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs index 0024813e..308b448f 100644 --- a/Text-Grab/Extensions/LanguageExtensions.cs +++ b/Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs @@ -1,6 +1,4 @@ using System; -using System.Globalization; -using System.Windows.Markup; using Text_Grab.Interfaces; using Text_Grab.Models; using Windows.Globalization; @@ -18,13 +16,6 @@ public static bool IsSpaceJoining(this Language selectedLanguage) return true; } - public static bool IsRightToLeft(this Language language) - { - XmlLanguage lang = XmlLanguage.GetLanguage(language.LanguageTag); - CultureInfo culture = lang.GetEquivalentCulture(); - return culture.TextInfo.IsRightToLeft; - } - public static bool IsSpaceJoining(this ILanguage selectedLanguage) { if (selectedLanguage.LanguageTag.StartsWith("zh", StringComparison.InvariantCultureIgnoreCase)) @@ -34,15 +25,6 @@ public static bool IsSpaceJoining(this ILanguage selectedLanguage) return true; } - public static bool IsRightToLeft(this ILanguage selectedLanguage) - { - if (selectedLanguage is GlobalLang language) - return language.OriginalLanguage.IsRightToLeft(); - - // For other language types, use the LayoutDirection property - return selectedLanguage.LayoutDirection == LanguageLayoutDirection.Rtl; - } - public static bool IsLatinBased(this ILanguage selectedLanguage) { return string.Equals(selectedLanguage.Script, "Latn", StringComparison.OrdinalIgnoreCase); diff --git a/Text-Grab/Extensions/LanguageRtlExtensions.cs b/Text-Grab/Extensions/LanguageRtlExtensions.cs new file mode 100644 index 00000000..f1b43776 --- /dev/null +++ b/Text-Grab/Extensions/LanguageRtlExtensions.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using System.Windows.Markup; +using Text_Grab.Interfaces; +using Text_Grab.Models; +using Windows.Globalization; + +namespace Text_Grab; + +/// +/// The WPF-bound half of what used to be Extensions/LanguageExtensions.cs. Split out because +/// comes from PresentationCore, so it cannot cross into +/// Text-Grab.Core.Windows (see docs/Core-Split-Plan.md, batch 3d). The portable members +/// (IsSpaceJoining, IsLatinBased, AsLanguage, AsILanguage) moved to Core.Windows keeping the +/// original LanguageExtensions type name so existing call sites resolve unchanged; these two +/// overloads stay here under a distinct name to avoid a same-named-extension-method ambiguity +/// (CS0121) between the two assemblies. +/// +public static class LanguageRtlExtensions +{ + public static bool IsRightToLeft(this Language language) + { + XmlLanguage lang = XmlLanguage.GetLanguage(language.LanguageTag); + CultureInfo culture = lang.GetEquivalentCulture(); + return culture.TextInfo.IsRightToLeft; + } + + public static bool IsRightToLeft(this ILanguage selectedLanguage) + { + if (selectedLanguage is GlobalLang language) + return language.OriginalLanguage.IsRightToLeft(); + + // For other language types, use the LayoutDirection property + return selectedLanguage.LayoutDirection == LanguageLayoutDirection.Rtl; + } +} From 90af6b84e49856d508d004ad41ed81df533c40b9 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 08:05:59 -0500 Subject: [PATCH 25/54] Record the RTL blocker batch 3d created for 4c BuildTextFromOcrLines is listed in 4c's portable subset but calls IsRightToLeft, which 3d left in the app with the rest of the XmlLanguage-bound code. Writing the three resolution options down now so 4c does not rediscover it. Co-Authored-By: Claude Opus 5 --- docs/Core-Split-Plan.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 361d4879..f27c1121 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -372,6 +372,23 @@ move only the strictly portable subset — furigana filtering, paragraph-wrap he `BuildTextFromOcrLines`, `GetStringFromOcrOutputs`, `GetTextFromOcrLine` — and leave engine dispatch behind. +**Blocker created by batch 3d — resolve before 4c.** `BuildTextFromOcrLines` is named above as +part of the "strictly portable subset", but `OcrUtilities.cs:642` calls `language.IsRightToLeft()`, +and 3d left both `IsRightToLeft` overloads in the app as `LanguageRtlExtensions` because +`XmlLanguage` comes from PresentationCore. `ReverseWordsForRightToLeft` on the next line is fine — +it is already in Core (`Extensions/StringBuilderExtensions.cs`). + +Only the `Language` overload actually needs `XmlLanguage`; the `ILanguage` overload needs it purely +because its `GlobalLang` branch delegates to the other one. Three options, in order of preference: +1. Give the `ILanguage` overload's `GlobalLang` branch a portable RTL check and move that overload + to Core.Windows, leaving the `Language`/`XmlLanguage` overload in the app. `XmlLanguage + .GetLanguage(tag).GetEquivalentCulture()` is close to `CultureInfo.GetCultureInfo(tag)` but not + identical — it walks up subtags and falls back to the invariant culture — so this is a real + behavior question, not a mechanical swap. Verify against RTL tags with subtags (`ar-EG`, + `he-IL`, `ur-PK`) before choosing it. +2. Route RTL through a settable hook the app registers at startup, same shape as `SettingsAccess`. +3. Leave `BuildTextFromOcrLines` in the app and shrink 4c's portable subset accordingly. + `Tests/OcrTests.cs` is the heaviest consumer of the headless surface and becomes a `Tests.Core.Windows` candidate in Wave 7. It references the nested `PositionedOcrLine` / `GroupedOcrLines` types by name; both halves keep `Text_Grab.Utilities`, so it stays green. From 93c1729fc730cac735b82ade184efd6a9000169c Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Thu, 27 Aug 2026 08:11:54 -0500 Subject: [PATCH 26/54] Move the automation settings provider and profile into Text-Grab.Core Batch 6a of the Core split, pulled ahead of Waves 4-5 because reconnaissance found it blocks TesseractHelper (4b), ContextMenuUtilities and FileAssociationUtilities (3a-deferred), and FileUtilities (5). - Text-Grab.Core/Utilities/AutomationSettingsProvider.cs - pure rename, zero content changes, exactly as the plan predicted. - Text-Grab.Core/Utilities/AutomationProfile.cs - pure rename plus one mechanical change: ApplySeed(Properties.Settings) widened to ApplySeed(ApplicationSettingsBase), since Properties.Settings is the app's internal, concrete ApplicationSettingsBase subclass and cannot move (see B1 in the plan). Every direct typed-property write (settings.FirstRun = false, etc.) became an indexer write (settings["FirstRun"] = false) - behavior-preserving, not just type erasure: Settings.Designer.cs generates each property as a thin wrapper whose setter is exactly `this["PropertyName"] = value;`, so the indexer call is the same call the typed property would have made. The file's own actual location was Utilities/, not Models/ as an earlier planning note assumed; the namespace (Text_Grab.Utilities) is unchanged either way per invariant 1, so it moved to Text-Grab.Core/Utilities/ to match. - Added System.Configuration.ConfigurationManager 10.0.11 to Text-Grab.Core.csproj. Confirmed empirically (not guessed): the app itself carries no explicit PackageReference for this package today - ApplicationSettingsBase/LocalFileSettingsProvider resolve there for free from the Microsoft.WindowsDesktop.App shared framework, which plain net10.0 Core does not get. 10.0.11 is nuget.org's latest stable 10.0.x release and matches the newest installed net10.0 runtime (10.0.11); restoring it against the live feed succeeded cleanly. Self-assembly-reflection audit (the specific failure mode that already bit GetAppVersion() and the LimitedAccessFeatureUtilities LAF token earlier in this reorganization): grepped both moved files for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x) .Assembly, Assembly.GetName/.Location/.CodeBase, and AssemblyMetadata - zero matches in either file. The one place this class of bug could still hide is the LocalFileSettingsProvider base class's own user.config path resolution (used when AutomationProfile.Current is null), but that resolution keys off the process's entry assembly, not the declaring assembly of the SettingsProvider type - moving the provider to a different assembly does not change what the running Text-Grab.exe process's entry assembly is, so existing users' user.config is not orphaned by this move. No files deferred from this batch's list; both files moved cleanly. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing WinRT.Runtime/Vortice MSB3277 conflict warnings only, unrelated to this batch). Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Verified specifically that AutomationProfileTests, AutomationSettingsProviderTests (including the SettingsService seeding round-trip that exercises the widened ApplySeed) and SettingsAccessTests are all in the passing set. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp --- Text-Grab.Core/Text-Grab.Core.csproj | 7 ++++ .../Utilities/AutomationProfile.cs | 33 +++++++++++-------- .../Utilities/AutomationSettingsProvider.cs | 0 3 files changed, 27 insertions(+), 13 deletions(-) rename {Text-Grab => Text-Grab.Core}/Utilities/AutomationProfile.cs (88%) rename {Text-Grab => Text-Grab.Core}/Utilities/AutomationSettingsProvider.cs (100%) diff --git a/Text-Grab.Core/Text-Grab.Core.csproj b/Text-Grab.Core/Text-Grab.Core.csproj index 7ed96db7..5ef1b989 100644 --- a/Text-Grab.Core/Text-Grab.Core.csproj +++ b/Text-Grab.Core/Text-Grab.Core.csproj @@ -25,6 +25,13 @@ none + +
diff --git a/Text-Grab/Utilities/AutomationProfile.cs b/Text-Grab.Core/Utilities/AutomationProfile.cs similarity index 88% rename from Text-Grab/Utilities/AutomationProfile.cs rename to Text-Grab.Core/Utilities/AutomationProfile.cs index 42c34ae4..ddf23dc6 100644 --- a/Text-Grab/Utilities/AutomationProfile.cs +++ b/Text-Grab.Core/Utilities/AutomationProfile.cs @@ -164,20 +164,27 @@ internal static string GetTemporaryFilePath(string extension = ".tmp") return Path.Combine(GetTemporaryDirectory(), $"{Guid.NewGuid():N}{normalizedExtension}"); } - internal void ApplySeed(Properties.Settings settings) + // Widened from Properties.Settings (the app's concrete, internal ApplicationSettingsBase + // subclass) so this can move to Core, which cannot reference the app assembly. Every + // property below is written through the SettingsBase indexer instead of a generated typed + // property. This is behavior-preserving, not just type-erasure: each generated property in + // Settings.Designer.cs (e.g. `FirstRun`) is a thin wrapper whose setter is exactly + // `this["FirstRun"] = value;` - the indexer assignment below is the same call the typed + // property would have made. + internal void ApplySeed(ApplicationSettingsBase settings) { - settings.FirstRun = false; - settings.RunInTheBackground = false; - settings.StartupOnLogin = false; - settings.GlobalHotkeysEnabled = false; - settings.ShowToast = false; - settings.DefaultLaunch = TextGrabMode.EditText.ToString(); - settings.LastUsedLang = "en-US"; - settings.UseTesseract = false; - settings.UiAutomationEnabled = false; - settings.WindowsAiDescriptionEnabled = false; - settings.EnableFileBackedManagedSettings = true; - settings.LookupFileLocation = LookupFilePath; + settings["FirstRun"] = false; + settings["RunInTheBackground"] = false; + settings["StartupOnLogin"] = false; + settings["GlobalHotkeysEnabled"] = false; + settings["ShowToast"] = false; + settings["DefaultLaunch"] = TextGrabMode.EditText.ToString(); + settings["LastUsedLang"] = "en-US"; + settings["UseTesseract"] = false; + settings["UiAutomationEnabled"] = false; + settings["WindowsAiDescriptionEnabled"] = false; + settings["EnableFileBackedManagedSettings"] = true; + settings["LookupFileLocation"] = LookupFilePath; foreach ((string propertyName, JsonElement value) in _seedValues) { diff --git a/Text-Grab/Utilities/AutomationSettingsProvider.cs b/Text-Grab.Core/Utilities/AutomationSettingsProvider.cs similarity index 100% rename from Text-Grab/Utilities/AutomationSettingsProvider.cs rename to Text-Grab.Core/Utilities/AutomationSettingsProvider.cs From 0d3b48516d901ca3d8e6a60ad768b9f63846e98f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 22:47:46 -0500 Subject: [PATCH 27/54] Move OcrOutput and BarcodeUtilities into Text-Grab.Core.Windows Batch 4a of the Core split. Both files were already effectively pure Core.Windows types - ILanguage/OcrEngineKind/OcrOutputKind were already there or in Core, and Bitmap/SoftwareBitmap (GDI+ / WinRT) are fine in Core.Windows per B3. - Models/OcrOutput.cs: CleanOutput() swapped AppUtilities.TextGrabSettings (the app's internal Settings type, via an `is not Settings userSettings` cast) for SettingsAccess.Current, reading CorrectToLatin/CorrectErrors - both already on ITextGrabSettings from Wave 0. No interface changes needed. - Utilities/BarcodeUtilities.cs moved as-is - pure GDI+ Bitmap and ZXing, no settings or WPF touchpoints. Added ZXing.Net.Bindings.Windows.Compatibility to Text-Grab.Core.Windows.csproj for BarcodeReader/BarcodeWriter. Left ZXing.Net and ZXing.Net.Bindings.Windows.Compatibility in Text-Grab.csproj unchanged - verified by grep that QrCodeWindow.xaml.cs and GrabFrame.xaml.cs still reference ZXing types directly. Self-assembly-reflection audit: grepped both files for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x) .Assembly, Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and AppContext.BaseDirectory - zero matches in either file. No files deferred from this batch's list; both moved cleanly. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 assembly-conflict warnings only). Co-Authored-By: Claude Opus 5 --- .../Models/OcrOutput.cs | 9 ++++----- Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj | 2 ++ .../Utilities/BarcodeUtilities.cs | 0 3 files changed, 6 insertions(+), 5 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Models/OcrOutput.cs (77%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/BarcodeUtilities.cs (100%) diff --git a/Text-Grab/Models/OcrOutput.cs b/Text-Grab.Core.Windows/Models/OcrOutput.cs similarity index 77% rename from Text-Grab/Models/OcrOutput.cs rename to Text-Grab.Core.Windows/Models/OcrOutput.cs index eaf900cf..4587709f 100644 --- a/Text-Grab/Models/OcrOutput.cs +++ b/Text-Grab.Core.Windows/Models/OcrOutput.cs @@ -1,6 +1,6 @@ using System.Drawing; using Text_Grab.Interfaces; -using Text_Grab.Properties; +using Text_Grab.Services; using Text_Grab.Utilities; using Windows.Graphics.Imaging; @@ -18,16 +18,15 @@ public record OcrOutput public void CleanOutput() { - if (AppUtilities.TextGrabSettings is not Settings userSettings - || Kind == OcrOutputKind.Barcode) + if (Kind == OcrOutputKind.Barcode) return; string correctingString = RawOutput; - if (userSettings.CorrectToLatin && Language?.IsLatinBased() == true) + if (SettingsAccess.Current.CorrectToLatin && Language?.IsLatinBased() == true) correctingString = correctingString.ReplaceGreekOrCyrillicWithLatin(); - if (userSettings.CorrectErrors) + if (SettingsAccess.Current.CorrectErrors) correctingString = correctingString.TryFixEveryWordLetterNumberErrors(); CleanedOutput = correctingString; diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index 334e86c4..a1360a2f 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -46,6 +46,8 @@ + + diff --git a/Text-Grab/Utilities/BarcodeUtilities.cs b/Text-Grab.Core.Windows/Utilities/BarcodeUtilities.cs similarity index 100% rename from Text-Grab/Utilities/BarcodeUtilities.cs rename to Text-Grab.Core.Windows/Utilities/BarcodeUtilities.cs From cc9c581f1adcf8642ec0ceeb90549d8dc9d7992d Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 22:48:42 -0500 Subject: [PATCH 28/54] Move TesseractHelper into Text-Grab.Core.Windows, split out the downloader Batch 4b of the Core split. TesseractHelper.cs was blocked only on AutomationProfile via TempImagePath() - that landed in Text-Grab.Core in 93c1729, so this batch was purely mechanical: - TesseractHelper: dropped the cached `Settings DefaultSettings` field (AppUtilities.TextGrabSettings, the app's internal settings type) and read SettingsAccess.Current inline in GetTesseractPath() instead, same shape as every other moved settings touchpoint - TesseractPath and Save() are already on ITextGrabSettings, no interface change needed. AutomationProfile.Current/.GetTemporaryDirectory() in TempImagePath() need no using - same namespace (Text_Grab.Utilities), same as the app. - TesseractGitHubFileDownloader (the second class in the same file) was fully portable - HttpClient, File I/O, no Windows types - so it split out to Text-Grab.Core/Utilities/TesseractGitHubFileDownloader.cs rather than riding along in Core.Windows. Namespace kept as Text_Grab.Utilities per invariant 1; its two app call sites (LanguageSettings.xaml.cs) and the test file (OcrTests.cs) need no changes since the app already references Core.Windows -> Core transitively. Added CliWrap to Text-Grab.Core.Windows.csproj. Left CliWrap in Text-Grab.csproj unchanged - QuickSimpleLookup.xaml.cs still uses it directly. Self-assembly-reflection audit: grepped both files for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x) .Assembly, Assembly.GetName/.Location/.CodeBase, and AssemblyMetadata - zero matches. TempImagePath()'s System.AppContext.BaseDirectory fallback (used only when AutomationProfile.Current is null) is process entry-point based, not declaring-assembly based, so it means the same thing after the move - same reasoning as the LocalFileSettingsProvider check in 93c1729. No files deferred from this batch's list; TesseractHelper moved cleanly and the downloader split cleanly. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 assembly-conflict warnings only). Co-Authored-By: Claude Opus 5 --- .../Text-Grab.Core.Windows.csproj | 2 + .../Utilities/TesseractHelper.cs | 195 ++---------------- .../TesseractGitHubFileDownloader.cs | 175 ++++++++++++++++ 3 files changed, 189 insertions(+), 183 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/TesseractHelper.cs (53%) create mode 100644 Text-Grab.Core/Utilities/TesseractGitHubFileDownloader.cs diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index a1360a2f..4e33bb6e 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -48,6 +48,8 @@ + + diff --git a/Text-Grab/Utilities/TesseractHelper.cs b/Text-Grab.Core.Windows/Utilities/TesseractHelper.cs similarity index 53% rename from Text-Grab/Utilities/TesseractHelper.cs rename to Text-Grab.Core.Windows/Utilities/TesseractHelper.cs index 12482112..f54a131e 100644 --- a/Text-Grab/Utilities/TesseractHelper.cs +++ b/Text-Grab.Core.Windows/Utilities/TesseractHelper.cs @@ -6,13 +6,12 @@ using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Text_Grab.Interfaces; using Text_Grab.Models; -using Text_Grab.Properties; +using Text_Grab.Services; namespace Text_Grab.Utilities; @@ -30,9 +29,6 @@ public static class TesseractHelper private const string rawProgramsPath = @"%LOCALAPPDATA%\Programs\Tesseract-OCR\tesseract.exe"; private const string basicPath = @"C:\Program Files\Tesseract-OCR\tesseract.exe"; - private static readonly Settings DefaultSettings = AppUtilities.TextGrabSettings; - - public static bool CanLocateTesseractExe() { string tesseractPath = string.Empty; @@ -52,31 +48,33 @@ public static bool CanLocateTesseractExe() private static string GetTesseractPath() { - if (!string.IsNullOrWhiteSpace(DefaultSettings.TesseractPath) - && File.Exists(DefaultSettings.TesseractPath)) - return DefaultSettings.TesseractPath; + ITextGrabSettings defaultSettings = SettingsAccess.Current; + + if (!string.IsNullOrWhiteSpace(defaultSettings.TesseractPath) + && File.Exists(defaultSettings.TesseractPath)) + return defaultSettings.TesseractPath; string tesExePath = Environment.ExpandEnvironmentVariables(rawPath); string programsPath = Environment.ExpandEnvironmentVariables(rawProgramsPath); if (File.Exists(tesExePath)) { - DefaultSettings.TesseractPath = tesExePath; - DefaultSettings.Save(); + defaultSettings.TesseractPath = tesExePath; + defaultSettings.Save(); return tesExePath; } if (File.Exists(programsPath)) { - DefaultSettings.TesseractPath = programsPath; - DefaultSettings.Save(); + defaultSettings.TesseractPath = programsPath; + defaultSettings.Save(); return programsPath; } if (File.Exists(basicPath)) { - DefaultSettings.TesseractPath = basicPath; - DefaultSettings.Save(); + defaultSettings.TesseractPath = basicPath; + defaultSettings.Save(); return basicPath; } @@ -230,172 +228,3 @@ public static async Task> TesseractLanguages() return tesseractLanguages; } } - -public class TesseractGitHubFileDownloader -{ - private readonly HttpClient _client; - - public TesseractGitHubFileDownloader() - { - _client = new HttpClient(); - // It's a good practice to set a user-agent when making requests - _client.DefaultRequestHeaders.Add("User-Agent", "Text Grab settings language downloader"); - } - - public async Task DownloadFileAsync(string filenameToDownload, string localDestination) - { - // Construct the URL to the raw content of the file in the GitHub repository - // https://github.com/tesseract-ocr/tessdata - string fileUrl = $"https://raw.githubusercontent.com/tesseract-ocr/tessdata/main/{filenameToDownload}"; - - try - { - // Send a GET request to the specified URL - HttpResponseMessage response = await _client.GetAsync(fileUrl); - response.EnsureSuccessStatusCode(); - - // Read the response content - byte[] fileContents = await response.Content.ReadAsByteArrayAsync(); - - // Write the content to a file on the local file system - await File.WriteAllBytesAsync(localDestination, fileContents); - Console.WriteLine("File downloaded successfully."); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred: {ex.Message}"); - } - } - - public static readonly string[] tesseractTrainedDataFileNames = [ - "afr.traineddata", - "amh.traineddata", - "ara.traineddata", - "asm.traineddata", - "aze.traineddata", - "aze_cyrl.traineddata", - "bel.traineddata", - "ben.traineddata", - "bod.traineddata", - "bos.traineddata", - "bre.traineddata", - "bul.traineddata", - "cat.traineddata", - "ceb.traineddata", - "ces.traineddata", - "chi_sim.traineddata", - "chi_sim_vert.traineddata", - "chi_tra.traineddata", - "chi_tra_vert.traineddata", - "chr.traineddata", - "cos.traineddata", - "cym.traineddata", - "dan.traineddata", - "dan_frak.traineddata", - "deu.traineddata", - "deu_frak.traineddata", - "div.traineddata", - "dzo.traineddata", - "ell.traineddata", - "eng.traineddata", - "enm.traineddata", - "epo.traineddata", - "equ.traineddata", - "est.traineddata", - "eus.traineddata", - "fao.traineddata", - "fas.traineddata", - "fil.traineddata", - "fin.traineddata", - "fra.traineddata", - "frk.traineddata", - "frm.traineddata", - "fry.traineddata", - "gla.traineddata", - "gle.traineddata", - "glg.traineddata", - "grc.traineddata", - "guj.traineddata", - "hat.traineddata", - "heb.traineddata", - "hin.traineddata", - "hrv.traineddata", - "hun.traineddata", - "hye.traineddata", - "iku.traineddata", - "ind.traineddata", - "isl.traineddata", - "ita.traineddata", - "ita_old.traineddata", - "jav.traineddata", - "jpn.traineddata", - "jpn_vert.traineddata", - "kan.traineddata", - "kat.traineddata", - "kat_old.traineddata", - "kaz.traineddata", - "khm.traineddata", - "kir.traineddata", - "kmr.traineddata", - "kor.traineddata", - "kor_vert.traineddata", - "lao.traineddata", - "lat.traineddata", - "lav.traineddata", - "lit.traineddata", - "ltz.traineddata", - "mal.traineddata", - "mar.traineddata", - "mkd.traineddata", - "mlt.traineddata", - "mon.traineddata", - "mri.traineddata", - "msa.traineddata", - "mya.traineddata", - "nep.traineddata", - "nld.traineddata", - "nor.traineddata", - "oci.traineddata", - "ori.traineddata", - "osd.traineddata", - "pan.traineddata", - "pol.traineddata", - "por.traineddata", - "pus.traineddata", - "que.traineddata", - "ron.traineddata", - "rus.traineddata", - "san.traineddata", - "sin.traineddata", - "slk.traineddata", - "slk_frak.traineddata", - "slv.traineddata", - "snd.traineddata", - "spa.traineddata", - "spa_old.traineddata", - "sqi.traineddata", - "srp.traineddata", - "srp_latn.traineddata", - "sun.traineddata", - "swa.traineddata", - "swe.traineddata", - "syr.traineddata", - "tam.traineddata", - "tat.traineddata", - "tel.traineddata", - "tgk.traineddata", - "tgl.traineddata", - "tha.traineddata", - "tir.traineddata", - "ton.traineddata", - "tur.traineddata", - "uig.traineddata", - "ukr.traineddata", - "urd.traineddata", - "uzb.traineddata", - "uzb_cyrl.traineddata", - "vie.traineddata", - "yid.traineddata", - "yor.traineddata", - ]; -} diff --git a/Text-Grab.Core/Utilities/TesseractGitHubFileDownloader.cs b/Text-Grab.Core/Utilities/TesseractGitHubFileDownloader.cs new file mode 100644 index 00000000..b53ac6bf --- /dev/null +++ b/Text-Grab.Core/Utilities/TesseractGitHubFileDownloader.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Text_Grab.Utilities; + +public class TesseractGitHubFileDownloader +{ + private readonly HttpClient _client; + + public TesseractGitHubFileDownloader() + { + _client = new HttpClient(); + // It's a good practice to set a user-agent when making requests + _client.DefaultRequestHeaders.Add("User-Agent", "Text Grab settings language downloader"); + } + + public async Task DownloadFileAsync(string filenameToDownload, string localDestination) + { + // Construct the URL to the raw content of the file in the GitHub repository + // https://github.com/tesseract-ocr/tessdata + string fileUrl = $"https://raw.githubusercontent.com/tesseract-ocr/tessdata/main/{filenameToDownload}"; + + try + { + // Send a GET request to the specified URL + HttpResponseMessage response = await _client.GetAsync(fileUrl); + response.EnsureSuccessStatusCode(); + + // Read the response content + byte[] fileContents = await response.Content.ReadAsByteArrayAsync(); + + // Write the content to a file on the local file system + await File.WriteAllBytesAsync(localDestination, fileContents); + Console.WriteLine("File downloaded successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + } + + public static readonly string[] tesseractTrainedDataFileNames = [ + "afr.traineddata", + "amh.traineddata", + "ara.traineddata", + "asm.traineddata", + "aze.traineddata", + "aze_cyrl.traineddata", + "bel.traineddata", + "ben.traineddata", + "bod.traineddata", + "bos.traineddata", + "bre.traineddata", + "bul.traineddata", + "cat.traineddata", + "ceb.traineddata", + "ces.traineddata", + "chi_sim.traineddata", + "chi_sim_vert.traineddata", + "chi_tra.traineddata", + "chi_tra_vert.traineddata", + "chr.traineddata", + "cos.traineddata", + "cym.traineddata", + "dan.traineddata", + "dan_frak.traineddata", + "deu.traineddata", + "deu_frak.traineddata", + "div.traineddata", + "dzo.traineddata", + "ell.traineddata", + "eng.traineddata", + "enm.traineddata", + "epo.traineddata", + "equ.traineddata", + "est.traineddata", + "eus.traineddata", + "fao.traineddata", + "fas.traineddata", + "fil.traineddata", + "fin.traineddata", + "fra.traineddata", + "frk.traineddata", + "frm.traineddata", + "fry.traineddata", + "gla.traineddata", + "gle.traineddata", + "glg.traineddata", + "grc.traineddata", + "guj.traineddata", + "hat.traineddata", + "heb.traineddata", + "hin.traineddata", + "hrv.traineddata", + "hun.traineddata", + "hye.traineddata", + "iku.traineddata", + "ind.traineddata", + "isl.traineddata", + "ita.traineddata", + "ita_old.traineddata", + "jav.traineddata", + "jpn.traineddata", + "jpn_vert.traineddata", + "kan.traineddata", + "kat.traineddata", + "kat_old.traineddata", + "kaz.traineddata", + "khm.traineddata", + "kir.traineddata", + "kmr.traineddata", + "kor.traineddata", + "kor_vert.traineddata", + "lao.traineddata", + "lat.traineddata", + "lav.traineddata", + "lit.traineddata", + "ltz.traineddata", + "mal.traineddata", + "mar.traineddata", + "mkd.traineddata", + "mlt.traineddata", + "mon.traineddata", + "mri.traineddata", + "msa.traineddata", + "mya.traineddata", + "nep.traineddata", + "nld.traineddata", + "nor.traineddata", + "oci.traineddata", + "ori.traineddata", + "osd.traineddata", + "pan.traineddata", + "pol.traineddata", + "por.traineddata", + "pus.traineddata", + "que.traineddata", + "ron.traineddata", + "rus.traineddata", + "san.traineddata", + "sin.traineddata", + "slk.traineddata", + "slk_frak.traineddata", + "slv.traineddata", + "snd.traineddata", + "spa.traineddata", + "spa_old.traineddata", + "sqi.traineddata", + "srp.traineddata", + "srp_latn.traineddata", + "sun.traineddata", + "swa.traineddata", + "swe.traineddata", + "syr.traineddata", + "tam.traineddata", + "tat.traineddata", + "tel.traineddata", + "tgk.traineddata", + "tgl.traineddata", + "tha.traineddata", + "tir.traineddata", + "ton.traineddata", + "tur.traineddata", + "uig.traineddata", + "ukr.traineddata", + "urd.traineddata", + "uzb.traineddata", + "uzb_cyrl.traineddata", + "vie.traineddata", + "yid.traineddata", + "yor.traineddata", + ]; +} From bd84507e5f4661fcbef8281e46541b6bf47161c0 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:01:02 -0500 Subject: [PATCH 29/54] Delete dead ResultTable code, move the clustering algorithm to Core Batch 4d of the Core split. Deleted first, as its own step, after re-verifying zero call sites for each (the OcrResult property was private, ParseOcrResultWordsIntoRects/ CalculateResultRows/MergeTheseRowIDs were private with no internal callers, and the ref-parameter constructor was never called - GrabFrame's `AnalyzedResultTable = new()` and `ResultTable tmp = new()` both use the parameterless constructor): - The `OcrResult` property and `ParseOcrResultWordsIntoRects()` - The `ResultTable(ref List, DpiScale)` constructor - `CalculateResultRows` and `MergeTheseRowIDs` Then split what was left. `WordBorderInfo` (2d) and `GrabFrameTableEditState` (2b) were already in Core, so the clustering algorithm itself - row/column center-clustering, manual separator handling, and the pure text-from-cells serializer - had no remaining blocker. Two things did: - `BoundingRect` was `System.Windows.Rect` (WindowsBase, not portable). Changed to `System.Drawing.RectangleF`, the established B2 currency - same conversion WordBorderInfo.BorderRect already made. GrabFrame's one external read (`AnalyzedResultTable.BoundingRect`) picked up `.AsRect()` at the call site. - `TableLines`/`DrawTable()` build a WPF Canvas - genuinely app-bound, not part of the algorithm. Extracted to a new app-side Text-Grab/Utilities/ResultTableRenderer.cs (`BuildTableLines`), called from GrabFrame right after `AnalyzeAsTable` instead of internally via a `drawTable` bool. That bool and the two `AnalyzeAsTable` overloads that carried it are gone (both overloads now just do the analysis); the six test/benchmark call sites that passed `drawTable: false` had that argument dropped, and GrabFrame's `RemoveTableLines()` still finds the canvas by its unchanged "TableLines" Tag string. `ParseOcrResultIntoWordBorderInfos` did not move - it depends on `IOcrLinesWords`/`IOcrLine` (Text-Grab.Core.Windows, fine on its own) but also calls `GetTextFromOcrLine`, an app-only extension method in OcrUtilities.cs that reads settings and is itself blocked pending Wave 4c (OcrUtilities.cs as a whole, per the deferred ledger). That's a blocker outside this batch's file list, so the method moved sideways instead of up: into OcrUtilities.cs itself, next to GetTextFromOcrLine, with its unused `DpiScale dpi` parameter dropped (never read in the body - the `Rect` it built was immediately discarded into a `RectangleF` anyway, so the method now builds the RectangleF directly). Updated its four call sites (two in OcrUtilities.cs, two in Tests/OcrTests.cs) and removed the two now-unused DpiScale locals that fed it. Self-assembly-reflection audit: grepped every file touched for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x) .Assembly, Assembly.GetName/.Location/.CodeBase, and AssemblyMetadata - zero matches. No files deferred from this batch's list - ResultTable.cs moved and split in full; nothing added to section 7. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 warnings only). Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. No test deleted. Co-Authored-By: Claude Opus 5 --- Tests/OcrTests.cs | 6 +- Tests/ResultTableBenchmarks.cs | 4 +- Tests/ResultTableManualSeparatorTests.cs | 10 +- .../Models/ResultTable.cs | 265 +----------------- Text-Grab/Utilities/OcrUtilities.cs | 47 +++- Text-Grab/Utilities/ResultTableRenderer.cs | 69 +++++ Text-Grab/Views/GrabFrame.xaml.cs | 5 +- 7 files changed, 124 insertions(+), 282 deletions(-) rename {Text-Grab => Text-Grab.Core}/Models/ResultTable.cs (73%) create mode 100644 Text-Grab/Utilities/ResultTableRenderer.cs diff --git a/Tests/OcrTests.cs b/Tests/OcrTests.cs index a8e44328..52c08362 100644 --- a/Tests/OcrTests.cs +++ b/Tests/OcrTests.cs @@ -177,7 +177,6 @@ public async Task AnalyzeTable() // When IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(testBitmap, globalLang); - DpiScale dpi = new(1, 1); Rectangle rectCanvasSize = new() { Width = 1132, @@ -186,7 +185,7 @@ public async Task AnalyzeTable() Y = 0 }; - List wordBorders = ResultTable.ParseOcrResultIntoWordBorderInfos(ocrResult, dpi); + List wordBorders = OcrUtilities.ParseOcrResultIntoWordBorderInfos(ocrResult); ResultTable resultTable = new(); resultTable.AnalyzeAsTable(wordBorders, rectCanvasSize); @@ -509,7 +508,6 @@ 300 Brown // When IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(testBitmap, globalLang); - DpiScale dpi = new(1, 1); Rectangle rectCanvasSize = new() { Width = 1152, @@ -518,7 +516,7 @@ 300 Brown Y = 0 }; - List wordBorders = ResultTable.ParseOcrResultIntoWordBorderInfos(ocrResult, dpi); + List wordBorders = OcrUtilities.ParseOcrResultIntoWordBorderInfos(ocrResult); ResultTable resultTable = new(); resultTable.AnalyzeAsTable(wordBorders, rectCanvasSize); diff --git a/Tests/ResultTableBenchmarks.cs b/Tests/ResultTableBenchmarks.cs index 7b64c34b..e2d8c12d 100644 --- a/Tests/ResultTableBenchmarks.cs +++ b/Tests/ResultTableBenchmarks.cs @@ -61,7 +61,7 @@ public void Setup() // Warm-up analysis so we can benchmark text build in isolation too _resultTable = new ResultTable(); - _resultTable.AnalyzeAsTable(_syntheticBorders, _canvas, drawTable: false); + _resultTable.AnalyzeAsTable(_syntheticBorders, _canvas); } [Benchmark] @@ -79,7 +79,7 @@ public int AnalyzeAsTable_Baseline() } ResultTable rt = new(); - rt.AnalyzeAsTable(copy, _canvas, drawTable: false); + rt.AnalyzeAsTable(copy, _canvas); return rt.Rows.Count + rt.Columns.Count; } diff --git a/Tests/ResultTableManualSeparatorTests.cs b/Tests/ResultTableManualSeparatorTests.cs index 33905d4b..0cb5dd6f 100644 --- a/Tests/ResultTableManualSeparatorTests.cs +++ b/Tests/ResultTableManualSeparatorTests.cs @@ -16,7 +16,7 @@ public void AnalyzeAsTable_ManualRowSeparatorSplitsMergedRowOutput() ]; ResultTable automaticTable = new(); - automaticTable.AnalyzeAsTable(automaticInfos, new Rectangle(0, 0, 200, 200), drawTable: false); + automaticTable.AnalyzeAsTable(automaticInfos, new Rectangle(0, 0, 200, 200)); StringBuilder automaticText = new(); ResultTable.GetTextFromTabledWordBorders(automaticText, automaticInfos, true); @@ -33,8 +33,7 @@ public void AnalyzeAsTable_ManualRowSeparatorSplitsMergedRowOutput() manualInfos, new Rectangle(0, 0, 200, 200), manualRowSeparators: [18d], - manualColumnSeparators: null, - drawTable: false); + manualColumnSeparators: null); StringBuilder manualText = new(); ResultTable.GetTextFromTabledWordBorders(manualText, manualInfos, true); @@ -55,7 +54,7 @@ public void AnalyzeAsTable_ManualColumnSeparatorSplitsMergedColumnOutput() ]; ResultTable automaticTable = new(); - automaticTable.AnalyzeAsTable(automaticInfos, new Rectangle(0, 0, 200, 200), drawTable: false); + automaticTable.AnalyzeAsTable(automaticInfos, new Rectangle(0, 0, 200, 200)); StringBuilder automaticText = new(); ResultTable.GetTextFromTabledWordBorders(automaticText, automaticInfos, true); @@ -74,8 +73,7 @@ public void AnalyzeAsTable_ManualColumnSeparatorSplitsMergedColumnOutput() manualInfos, new Rectangle(0, 0, 200, 200), manualRowSeparators: null, - manualColumnSeparators: [25d], - drawTable: false); + manualColumnSeparators: [25d]); StringBuilder manualText = new(); ResultTable.GetTextFromTabledWordBorders(manualText, manualInfos, true); diff --git a/Text-Grab/Models/ResultTable.cs b/Text-Grab.Core/Models/ResultTable.cs similarity index 73% rename from Text-Grab/Models/ResultTable.cs rename to Text-Grab.Core/Models/ResultTable.cs index 15ddc827..846c981c 100644 --- a/Text-Grab/Models/ResultTable.cs +++ b/Text-Grab.Core/Models/ResultTable.cs @@ -3,12 +3,6 @@ using System.Drawing; using System.Linq; using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using Text_Grab.Utilities; -using Windows.Media.Ocr; -using Rect = System.Windows.Rect; namespace Text_Grab.Models; @@ -18,9 +12,7 @@ public class ResultTable public List Rows { get; set; } = []; - private OcrResult? OcrResult { get; set; } - - public Rect BoundingRect { get; set; } = new(); + public RectangleF BoundingRect { get; set; } = new(); public List ColumnLines { get; set; } = []; @@ -30,41 +22,11 @@ public class ResultTable public List RowLines { get; set; } = []; - public Canvas? TableLines { get; set; } = null; - public ResultTable() { } - // New: accept pure model objects - public ResultTable(ref List wordBorders, DpiScale dpiScale) - { - int borderBuffer = 3; - - Rectangle bordersBorder = new(); - if (wordBorders.Count > 0) - { - double leftsMin = wordBorders.Select(x => x.BorderRect.Left).Min(); - double topsMin = wordBorders.Select(x => x.BorderRect.Top).Min(); - double rightsMax = wordBorders.Select(x => x.BorderRect.Right).Max(); - double bottomsMax = wordBorders.Select(x => x.BorderRect.Bottom).Max(); - - bordersBorder = new() - { - X = (int)leftsMin - borderBuffer, - Y = (int)topsMin - borderBuffer, - Width = (int)(rightsMax + borderBuffer), - Height = (int)(bottomsMax + borderBuffer) - }; - } - - bordersBorder.Width = (int)(bordersBorder.Width * dpiScale.DpiScaleX); - bordersBorder.Height = (int)(bordersBorder.Height * dpiScale.DpiScaleY); - - AnalyzeAsTable(wordBorders, bordersBorder); - } - private void ParseRowAndColumnLines() { // Draw Bounding Rect @@ -113,81 +75,17 @@ private void ParseRowAndColumnLines() } } - private List ParseOcrResultWordsIntoRects() - { - List allBoundingRects = []; - - if (OcrResult is null) - return allBoundingRects; - - foreach (OcrLine ocrLine in OcrResult.Lines) - { - foreach (OcrWord ocrWord in ocrLine.Words) - { - Rect ocrWordRect = new( - ocrWord.BoundingRect.X, - ocrWord.BoundingRect.Y, - ocrWord.BoundingRect.Width, - ocrWord.BoundingRect.Height); - - allBoundingRects.Add(ocrWordRect); - } - } - - return allBoundingRects; - } - - public static List ParseOcrResultIntoWordBorderInfos( - IOcrLinesWords ocrResult, - DpiScale dpi, - bool shouldCorrectToLatin = true) - { - List infos = []; - - foreach (IOcrLine ocrLine in ocrResult.Lines) - { - double top = ocrLine.Words.Select(x => x.BoundingBox.Top).Min(); - double bottom = ocrLine.Words.Select(x => x.BoundingBox.Bottom).Max(); - double left = ocrLine.Words.Select(x => x.BoundingBox.Left).Min(); - double right = ocrLine.Words.Select(x => x.BoundingBox.Right).Max(); - - Rect lineRect = new() - { - X = left, - Y = top, - Width = Math.Abs(right - left), - Height = Math.Abs(bottom - top) - }; - - StringBuilder lineText = new(); - ocrLine.GetTextFromOcrLine(true, lineText, shouldCorrectToLatin); - - WordBorderInfo info = new() - { - BorderRect = lineRect.AsRectangleF(), - Word = lineText.ToString().Trim(), - ResultRowID = 0, - ResultColumnID = 0 - }; - - infos.Add(info); - } - - return infos; - } - // New core analyzer that operates on WordBorderInfo (pure model) - public void AnalyzeAsTable(ICollection wordBorders, Rectangle rectCanvasSize, bool drawTable = true) + public void AnalyzeAsTable(ICollection wordBorders, Rectangle rectCanvasSize) { - AnalyzeAsTable(wordBorders, rectCanvasSize, null, null, drawTable); + AnalyzeAsTable(wordBorders, rectCanvasSize, null, null); } public void AnalyzeAsTable( ICollection wordBorders, Rectangle rectCanvasSize, IReadOnlyCollection? manualRowSeparators, - IReadOnlyCollection? manualColumnSeparators, - bool drawTable = true) + IReadOnlyCollection? manualColumnSeparators) { if (wordBorders == null || wordBorders.Count == 0) { @@ -224,8 +122,6 @@ public void AnalyzeAsTable( ParseRowAndColumnLines(); ApplyManualSeparators(manualRowSeparators, manualColumnSeparators); AssignWordBordersToFinalGrid(wordBorders); - if (drawTable) - DrawTable(); } private static List BuildRowsByCenterClustering(ICollection wordBorders, double centerThreshold, double medianHeight) @@ -403,95 +299,6 @@ private static double Median(IEnumerable source) return list[mid]; } - private static List CalculateResultRows(int hitGridSpacing, List rowAreas) - { - List resultRows = []; - int rowTop = 0; - int rowCount = 0; - for (int i = 0; i < rowAreas.Count; i++) - { - int thisLine = rowAreas[i]; - - // check if should set this as top - if (i == 0) - rowTop = thisLine; - else - { - int prevRow = rowAreas[i - 1]; - if (thisLine - prevRow != hitGridSpacing) - { - rowTop = thisLine; - } - } - - // check to see if at bottom of row - if (i == rowAreas.Count - 1) - { - resultRows.Add(new ResultRow { Top = rowTop, Bottom = thisLine, ID = rowCount }); - rowCount++; - } - else if (i + 1 < rowAreas.Count) - { - int nextRow = rowAreas[i + 1]; - if (nextRow - thisLine != hitGridSpacing) - { - resultRows.Add(new ResultRow { Top = rowTop, Bottom = thisLine, ID = rowCount }); - rowCount++; - } - } - } - - return resultRows; - } - - private void DrawTable() - { - // Draw the lines and bounds of the table - SolidColorBrush tableColor = new(System.Windows.Media.Color.FromArgb(255, 40, 118, 126)); - - TableLines = new Canvas() - { - Tag = "TableLines" - }; - - Border tableOutline = new() - { - Width = this.BoundingRect.Width, - Height = this.BoundingRect.Height, - BorderThickness = new Thickness(3), - BorderBrush = tableColor - }; - TableLines.Children.Add(tableOutline); - Canvas.SetTop(tableOutline, this.BoundingRect.Y); - Canvas.SetLeft(tableOutline, this.BoundingRect.X); - - foreach (double columnLine in this.ColumnLines) - { - Border vertLine = new() - { - Width = 2, - Height = this.BoundingRect.Height, - Background = tableColor - }; - TableLines.Children.Add(vertLine); - Canvas.SetTop(vertLine, this.BoundingRect.Y); - Canvas.SetLeft(vertLine, columnLine); - } - - foreach (double rowLine in this.RowLines) - { - Border horzLine = new() - { - Height = 2, - Width = this.BoundingRect.Width, - Background = tableColor - }; - TableLines.Children.Add(horzLine); - Canvas.SetTop(horzLine, rowLine); - Canvas.SetLeft(horzLine, this.BoundingRect.X); - } - } - // Build text from model-only borders public static void GetTextFromTabledWordBorders(StringBuilder stringBuilder, List wordBorders, bool isSpaceJoining) { @@ -762,70 +569,6 @@ private static bool LooksLikePlainNumber(string token) return t.All(ch => char.IsDigit(ch)); } - private static void MergeTheseRowIDs(List resultRows, List outlierRowIDs) - { - // Merge sparse rows into adjacent rows to reduce fragmentation - for (int i = 0; i < outlierRowIDs.Count; i++) - { - for (int j = 0; j < resultRows.Count; j++) - { - ResultRow jthRow = resultRows[j]; - if (jthRow.ID == outlierRowIDs[i]) - { - if (resultRows.Count == 1) - { - // nothing to merge - continue; - } - - if (j == 0) - { - // merge with next row if possible - if (j + 1 < resultRows.Count) - { - ResultRow nextRow = resultRows[j + 1]; - nextRow.Top = Math.Min(nextRow.Top, jthRow.Top); - } - } - else if (j == resultRows.Count - 1) - { - // merge with previous row - if (j - 1 >= 0) - { - ResultRow prevRow = resultRows[j - 1]; - prevRow.Bottom = Math.Max(prevRow.Bottom, jthRow.Bottom); - } - } - else - { - // merge with the closest neighbor by gap distance - ResultRow prevRow = resultRows[j - 1]; - ResultRow nextRow = resultRows[j + 1]; - int distToPrev = (int)(jthRow.Top - prevRow.Bottom); - int distToNext = (int)(nextRow.Top - jthRow.Bottom); - - if (distToNext < distToPrev) - { - // merge with next row - nextRow.Top = Math.Min(nextRow.Top, jthRow.Top); - } - else - { - // merge with prev row - prevRow.Bottom = Math.Max(prevRow.Bottom, jthRow.Bottom); - } - } - - resultRows.RemoveAt(j); - // reindex remaining IDs to keep them sequential - for (int k = 0; k < resultRows.Count; k++) - resultRows[k].ID = k; - break; - } - } - } - } - // Overload for WordBorderInfo private void AssignWordBordersToFinalGrid(ICollection wordBorders) { diff --git a/Text-Grab/Utilities/OcrUtilities.cs b/Text-Grab/Utilities/OcrUtilities.cs index a6621545..27c803d7 100644 --- a/Text-Grab/Utilities/OcrUtilities.cs +++ b/Text-Grab/Utilities/OcrUtilities.cs @@ -50,6 +50,45 @@ private static ILanguage GetCompatibleOcrLanguage(ILanguage language) return handle == IntPtr.Zero ? null : [handle]; } + // Moved from ResultTable when that class moved to Text-Grab.Core (batch 4d of the Core + // split): this stays app-side because it calls GetTextFromOcrLine below, an app-only + // extension method that reads settings and is itself blocked pending Wave 4c. + public static List ParseOcrResultIntoWordBorderInfos( + IOcrLinesWords ocrResult, + bool shouldCorrectToLatin = true) + { + List infos = []; + + foreach (IOcrLine ocrLine in ocrResult.Lines) + { + double top = ocrLine.Words.Select(x => x.BoundingBox.Top).Min(); + double bottom = ocrLine.Words.Select(x => x.BoundingBox.Bottom).Max(); + double left = ocrLine.Words.Select(x => x.BoundingBox.Left).Min(); + double right = ocrLine.Words.Select(x => x.BoundingBox.Right).Max(); + + RectangleF lineRect = new( + (float)left, + (float)top, + (float)Math.Abs(right - left), + (float)Math.Abs(bottom - top)); + + StringBuilder lineText = new(); + ocrLine.GetTextFromOcrLine(true, lineText, shouldCorrectToLatin); + + WordBorderInfo info = new() + { + BorderRect = lineRect, + Word = lineText.ToString().Trim(), + ResultRowID = 0, + ResultColumnID = 0 + }; + + infos.Add(info); + } + + return infos; + } + public static void GetTextFromOcrLine( this IOcrLine ocrLine, bool isSpaceJoiningOCRLang, @@ -205,13 +244,11 @@ public static async Task GetRegionsTextAsTableAsync(Window passedWindow, using Bitmap bmp = ImageMethods.GetRegionOfScreenAsBitmap(correctedRegion); double scale = await GetIdealScaleFactorForOcrAsync(bmp, compatibleLanguage); using Bitmap scaledBitmap = ImageMethods.ScaleBitmapUniform(bmp, scale); - DpiScale dpiScale = VisualTreeHelper.GetDpi(passedWindow); IOcrLinesWords ocrResult = await GetOcrResultFromImageAsync(scaledBitmap, compatibleLanguage); // New model-only flow - List wordBorderInfos = ResultTable.ParseOcrResultIntoWordBorderInfos( + List wordBorderInfos = ParseOcrResultIntoWordBorderInfos( ocrResult, - dpiScale, compatibleLanguage.IsLatinBased()); Rectangle rectCanvasSize = new() @@ -255,11 +292,9 @@ public static async Task GetTextFromBitmapAsTableAsync(Bitmap bitmap, IL double scale = await GetIdealScaleFactorForOcrAsync(bitmap, compatibleLanguage); using Bitmap scaledBitmap = ImageMethods.ScaleBitmapUniform(bitmap, scale); IOcrLinesWords ocrResult = await GetOcrResultFromImageAsync(scaledBitmap, compatibleLanguage); - DpiScale bitmapDpiScale = new(1.0, 1.0); - List wordBorderInfos = ResultTable.ParseOcrResultIntoWordBorderInfos( + List wordBorderInfos = ParseOcrResultIntoWordBorderInfos( ocrResult, - bitmapDpiScale, compatibleLanguage.IsLatinBased()); Rectangle rectCanvasSize = new() diff --git a/Text-Grab/Utilities/ResultTableRenderer.cs b/Text-Grab/Utilities/ResultTableRenderer.cs new file mode 100644 index 00000000..d8df3e22 --- /dev/null +++ b/Text-Grab/Utilities/ResultTableRenderer.cs @@ -0,0 +1,69 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Text_Grab.Extensions; +using Text_Grab.Models; + +namespace Text_Grab.Utilities; + +/// +/// Draws the visual grid lines for a onto a WPF . +/// +/// Split out of when that class moved to Text-Grab.Core (batch 4d of +/// the Core split) - the pure clustering algorithm and table state are portable, but this +/// rendering step needs Canvas/Border/SolidColorBrush, which are not. +/// +public static class ResultTableRenderer +{ + public static Canvas BuildTableLines(ResultTable table) + { + Rect boundingRect = table.BoundingRect.AsRect(); + + // Draw the lines and bounds of the table + SolidColorBrush tableColor = new(Color.FromArgb(255, 40, 118, 126)); + + Canvas tableLines = new() + { + Tag = "TableLines" + }; + + Border tableOutline = new() + { + Width = boundingRect.Width, + Height = boundingRect.Height, + BorderThickness = new Thickness(3), + BorderBrush = tableColor + }; + tableLines.Children.Add(tableOutline); + Canvas.SetTop(tableOutline, boundingRect.Y); + Canvas.SetLeft(tableOutline, boundingRect.X); + + foreach (double columnLine in table.ColumnLines) + { + Border vertLine = new() + { + Width = 2, + Height = boundingRect.Height, + Background = tableColor + }; + tableLines.Children.Add(vertLine); + Canvas.SetTop(vertLine, boundingRect.Y); + Canvas.SetLeft(vertLine, columnLine); + } + + foreach (double rowLine in table.RowLines) + { + Border horzLine = new() + { + Height = 2, + Width = boundingRect.Width, + Background = tableColor + }; + tableLines.Children.Add(horzLine); + Canvas.SetTop(horzLine, rowLine); + Canvas.SetLeft(horzLine, boundingRect.X); + } + + return tableLines; + } +} diff --git a/Text-Grab/Views/GrabFrame.xaml.cs b/Text-Grab/Views/GrabFrame.xaml.cs index 5e780867..c7cd4caf 100644 --- a/Text-Grab/Views/GrabFrame.xaml.cs +++ b/Text-Grab/Views/GrabFrame.xaml.cs @@ -740,7 +740,7 @@ private bool TryGetTablePlacementBounds(out Rect tableBounds) if (AnalyzedResultTable is null) _ = TryToPlaceTable(); - tableBounds = AnalyzedResultTable?.BoundingRect ?? Rect.Empty; + tableBounds = AnalyzedResultTable?.BoundingRect.AsRect() ?? Rect.Empty; return tableBounds != Rect.Empty && tableBounds.Width > 0 && tableBounds.Height > 0; @@ -4997,8 +4997,7 @@ private List TryToPlaceTable() tableEditState.SetManualSeparators( AnalyzedResultTable.ManualRowSeparators, AnalyzedResultTable.ManualColumnSeparators); - if (AnalyzedResultTable.TableLines is not null) - RectanglesCanvas.Children.Add(AnalyzedResultTable.TableLines); + RectanglesCanvas.Children.Add(ResultTableRenderer.BuildTableLines(AnalyzedResultTable)); } catch (Exception ex) { From 3b0bf02422f7e7f30629de3e5e588dbff6e71d46 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:12:41 -0500 Subject: [PATCH 30/54] Split OcrUtilities, move the OCR text assembly into Core.Windows Batch 4c of the Core split, plus the 3d RTL blocker it depended on. The split went the direction the call-site census pointed rather than the one the file's size suggests. Tests/OcrTests.cs alone held 43 of OcrUtilities' ~80 references and every one is against the text-assembly subset, so that half keeps the OcrUtilities name and moves to Text-Grab.Core.Windows: GetTextFromOcrLine, FilterFurigana, FilterFuriganaLines, OrderLinesForReadingFlow, BuildTextFromOcrLines, ShouldUseParagraphDetection, GroupWrappedParagraphLines, UnionRectangles, IsWrappedLine, IsWrappedParagraph, GetStringFromOcrOutputs, ParseOcrResultIntoWordBorderInfos, the nested PositionedOcrLine and GroupedOcrLines, and the SpaceJoiningWordRegex it all runs on. OcrTests therefore needed no edit for those, and rides into Tests.Core.Windows for free in 7a. The app-coupled half took the new name OcrSourceUtilities - screen and window capture, engine dispatch, file and BitmapSource sources. Its ~35 references across nine files were renamed. It still needs WPF throughout, and its engine dispatch needs WindowsAiUtilities and LanguageUtilities, neither of which has moved. The moved half's four settings reads (CorrectErrors, CorrectToLatin, ParagraphDetection, RemoveFurigana) were already on ITextGrabSettings, so this needed no interface addition - the static DefaultSettings field became SettingsAccess.Current reads, same as 4a and 4b. Deferred, as section 4.4 said to: LoadBitmapFromFile builds a WPF BitmapImage to apply EXIF rotation, so it and its two callers (OcrAbsoluteFilePathAsync, OcrFile) stay in the app. Section 7 updated. BuildTextFromOcrLines calls language.IsRightToLeft(), which 3d had left app-side because XmlLanguage comes from PresentationCore. Settled the behaviour question with a throwaway WPF probe rather than by reasoning: XmlLanguage.GetLanguage(tag).GetEquivalentCulture().TextInfo.IsRightToLeft and CultureInfo.GetCultureInfo(tag).TextInfo.IsRightToLeft agreed on all 24 tags probed - ar, ar-EG, ar-SA, he, he-IL, ur, ur-PK, fa, fa-IR, ckb, ps-AF, sd-Arab-PK, yi, he-Hebr-IL, ar-XX, en, en-US, ja, zh-Hans, de-DE, and the unresolvable xx, xx-YY, und and "". So the ILanguage overload moved into Core.Windows LanguageExtensions with a CultureInfo lookup, guarded by a CultureNotFoundException catch returning false (XmlLanguage fell back to the invariant culture, which is LTR). That left the Language overload with zero call sites - all five live IsRightToLeft calls are on ILanguage - so Extensions/LanguageRtlExtensions.cs is deleted outright. Deleted GetBoundingRect(this OcrLine) as section 8 dead code, re-verified first: WinRtOcrLinesWords has its own private GetBoundingRect and UIAutomationUtilities calls the unrelated TextPatternRange GetBoundingRectangles. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in any of the three files. Also recorded in the plan: 4e cannot precede 5a. LanguageService touches InputLanguageManager in exactly one private method and is otherwise WinRT, so the whole class can move behind a registered resolver - but GetAllLanguages and GetOCRLanguage call WindowsAiUtilities, which is deferred on SoftwareBitmapExtensions in 5a. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 --- Tests/OcrTests.cs | 24 +- .../Extensions/LanguageExtensions.cs | 39 +- .../Utilities/OcrUtilities.cs | 470 ++++++++++++++++ Text-Grab/App.xaml.cs | 2 +- Text-Grab/Controls/NotifyIconWindow.xaml.cs | 2 +- Text-Grab/Extensions/LanguageRtlExtensions.cs | 35 -- Text-Grab/Utilities/FileOpenUtilities.cs | 2 +- Text-Grab/Utilities/GrabTemplateExecutor.cs | 8 +- Text-Grab/Utilities/NotifyIconUtilities.cs | 2 +- ...{OcrUtilities.cs => OcrSourceUtilities.cs} | 502 +----------------- Text-Grab/Utilities/PdfDocumentRenderer.cs | 2 +- Text-Grab/Views/EditTextWindow.xaml.cs | 8 +- .../Views/FullscreenGrab.SelectionStyles.cs | 12 +- Text-Grab/Views/GrabFrame.xaml.cs | 6 +- docs/Core-Split-Plan.md | 59 +- 15 files changed, 608 insertions(+), 565 deletions(-) create mode 100644 Text-Grab.Core.Windows/Utilities/OcrUtilities.cs delete mode 100644 Text-Grab/Extensions/LanguageRtlExtensions.cs rename Text-Grab/Utilities/{OcrUtilities.cs => OcrSourceUtilities.cs} (55%) diff --git a/Tests/OcrTests.cs b/Tests/OcrTests.cs index 52c08362..a96e336e 100644 --- a/Tests/OcrTests.cs +++ b/Tests/OcrTests.cs @@ -142,7 +142,7 @@ public async Task OcrFontSampleImage() string testImagePath = fontSamplePath; // When - string ocrTextResult = await OcrUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); + string ocrTextResult = await OcrSourceUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); // Then Assert.Equal(fontSampleResult, ocrTextResult); @@ -157,7 +157,7 @@ public async Task OcrFontTestImage() Uri uri = new(testImagePath, UriKind.Relative); // When - string ocrTextResult = await OcrUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); + string ocrTextResult = await OcrSourceUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); // Then Assert.Equal(expectedResult, ocrTextResult); @@ -175,7 +175,7 @@ public async Task AnalyzeTable() GlobalLang globalLang = new(EnglishLanguage); Bitmap testBitmap = new(FileUtilities.GetPathToLocalFile(testImagePath)); // When - IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(testBitmap, globalLang); + IOcrLinesWords ocrResult = await OcrSourceUtilities.GetOcrResultFromImageAsync(testBitmap, globalLang); Rectangle rectCanvasSize = new() { @@ -211,7 +211,7 @@ public async Task ParagraphWrapDetection() try { // When - string ocrTextResult = await OcrUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); + string ocrTextResult = await OcrSourceUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); // Then Assert.Equal(expectedResult, ocrTextResult); @@ -382,7 +382,7 @@ public async Task OcrJapaneseImage_ReadingOrder_KeepsFuriganaWhenDisabled() try { // When - string ocrTextResult = await OcrUtilities.OcrAbsoluteFilePathAsync( + string ocrTextResult = await OcrSourceUtilities.OcrAbsoluteFilePathAsync( FileUtilities.GetPathToLocalFile(jaTestPath), japanese); // Then furigana are kept, but every line is in natural reading order @@ -411,7 +411,7 @@ public async Task OcrJapaneseImage_RemovesFuriganaWhenEnabled() try { // When - string ocrTextResult = await OcrUtilities.OcrAbsoluteFilePathAsync( + string ocrTextResult = await OcrSourceUtilities.OcrAbsoluteFilePathAsync( FileUtilities.GetPathToLocalFile(jaTestPath), japanese); // Then the furigana ruby lines are dropped, leaving the main text. @@ -435,9 +435,9 @@ public async Task InspectJapaneseOcrOutput() return; Bitmap testBitmap = new(FileUtilities.GetPathToLocalFile(jaTestPath)); - double scale = await OcrUtilities.GetIdealScaleFactorForOcrAsync(testBitmap, japanese); + double scale = await OcrSourceUtilities.GetIdealScaleFactorForOcrAsync(testBitmap, japanese); Bitmap scaledBitmap = ImageMethods.ScaleBitmapUniform(testBitmap, scale); - IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(scaledBitmap, japanese); + IOcrLinesWords ocrResult = await OcrSourceUtilities.GetOcrResultFromImageAsync(scaledBitmap, japanese); StringBuilder report = new(); report.AppendLine($"scale factor: {scale:0.###}"); @@ -481,7 +481,7 @@ public async Task ReadQrCode() string testImagePath = @".\Images\QrCodeTestImage.png"; Uri uri = new(testImagePath, UriKind.Relative); // When - string ocrTextResult = await OcrUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); + string ocrTextResult = await OcrSourceUtilities.OcrAbsoluteFilePathAsync(FileUtilities.GetPathToLocalFile(testImagePath)); // Then Assert.Equal(expectedResult, ocrTextResult); @@ -506,7 +506,7 @@ 300 Brown GlobalLang globalLang = new(EnglishLanguage); Bitmap testBitmap = new(FileUtilities.GetPathToLocalFile(testImagePath)); // When - IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(testBitmap, globalLang); + IOcrLinesWords ocrResult = await OcrSourceUtilities.GetOcrResultFromImageAsync(testBitmap, globalLang); Rectangle rectCanvasSize = new() { @@ -587,7 +587,7 @@ public async Task TesseractHocr() bmpImg.Freeze(); Bitmap bmp = ImageMethods.BitmapImageToBitmap(bmpImg); ILanguage language = LanguageUtilities.GetOCRLanguage(); - double idealScaleFactor = await OcrUtilities.GetIdealScaleFactorForOcrAsync(bmp, language); + double idealScaleFactor = await OcrSourceUtilities.GetIdealScaleFactorForOcrAsync(bmp, language); Bitmap scaledBMP = ImageMethods.ScaleBitmapUniform(bmp, idealScaleFactor); // When @@ -616,7 +616,7 @@ public async Task TesseractFontSample() bmpImg.Freeze(); Bitmap bmp = ImageMethods.BitmapImageToBitmap(bmpImg); ILanguage language = LanguageUtilities.GetOCRLanguage(); - double idealScaleFactor = await OcrUtilities.GetIdealScaleFactorForOcrAsync(bmp, language); + double idealScaleFactor = await OcrSourceUtilities.GetIdealScaleFactorForOcrAsync(bmp, language); Bitmap scaledBMP = ImageMethods.ScaleBitmapUniform(bmp, idealScaleFactor); // When diff --git a/Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs b/Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs index 308b448f..1b50eb3b 100644 --- a/Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs +++ b/Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Globalization; using Text_Grab.Interfaces; using Text_Grab.Models; using Windows.Globalization; @@ -53,4 +54,40 @@ public static bool IsLatinBased(this ILanguage selectedLanguage) return null; return new GlobalLang(language); } + + /// + /// Whether text in this language reads right-to-left. + /// + /// The GlobalLang branch used to delegate to an overload taking + /// Windows.Globalization.Language, which resolved the tag through + /// XmlLanguage.GetLanguage(tag).GetEquivalentCulture(). XmlLanguage comes from + /// PresentationCore, which is why batch 3d had to leave both overloads in the app. Batch 4c + /// needed this one in Core.Windows for BuildTextFromOcrLines, so the tag is now resolved with + /// CultureInfo directly. The two were probed against 24 tags - ar, ar-EG, ar-SA, he, he-IL, + /// ur, ur-PK, fa, fa-IR, ckb, ps-AF, sd-Arab-PK, yi, he-Hebr-IL, ar-XX, en, en-US, ja, + /// zh-Hans, de-DE, and the unresolvable xx, xx-YY, und and "" - and agreed on every one. + /// + public static bool IsRightToLeft(this ILanguage selectedLanguage) + { + if (selectedLanguage is GlobalLang language) + return IsRightToLeftTag(language.OriginalLanguage.LanguageTag); + + // For other language types, use the LayoutDirection property + return selectedLanguage.LayoutDirection == LanguageLayoutDirection.Rtl; + } + + private static bool IsRightToLeftTag(string languageTag) + { + try + { + return CultureInfo.GetCultureInfo(languageTag).TextInfo.IsRightToLeft; + } + catch (CultureNotFoundException) + { + // XmlLanguage fell back to the invariant culture, which is left-to-right, for tags + // it could not resolve. Keep that behaviour rather than throwing at a call site that + // only wanted to know which way to order words. + return false; + } + } } diff --git a/Text-Grab.Core.Windows/Utilities/OcrUtilities.cs b/Text-Grab.Core.Windows/Utilities/OcrUtilities.cs new file mode 100644 index 00000000..4a54945d --- /dev/null +++ b/Text-Grab.Core.Windows/Utilities/OcrUtilities.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Text_Grab.Interfaces; +using Text_Grab.Models; +using Text_Grab.Services; + +namespace Text_Grab.Utilities; + +/// +/// Turning an OCR result into text: word- and line-level assembly, the furigana and reading-flow +/// heuristics, and the paragraph-wrap grouping. +/// +/// The portable half of what used to be Text-Grab/Utilities/OcrUtilities.cs (batch 4c of the Core +/// split). It keeps the original type name because that is what nearly every call site wants - +/// Tests/OcrTests.cs alone accounts for 43 of the old file's ~80 references, all against these +/// members. The other half - screen and window capture, engine dispatch, file and BitmapSource +/// sources - stays in the app as OcrSourceUtilities: it needs WPF, and also WindowsAiUtilities +/// and LanguageUtilities, neither of which has moved yet. +/// +public static partial class OcrUtilities +{ + // Cache the SpaceJoiningWordRegex to avoid creating it on every method call + private static readonly Regex _cachedSpaceJoiningWordRegex = SpaceJoiningWordRegex(); + + public static List ParseOcrResultIntoWordBorderInfos( + IOcrLinesWords ocrResult, + bool shouldCorrectToLatin = true) + { + List infos = []; + + foreach (IOcrLine ocrLine in ocrResult.Lines) + { + double top = ocrLine.Words.Select(x => x.BoundingBox.Top).Min(); + double bottom = ocrLine.Words.Select(x => x.BoundingBox.Bottom).Max(); + double left = ocrLine.Words.Select(x => x.BoundingBox.Left).Min(); + double right = ocrLine.Words.Select(x => x.BoundingBox.Right).Max(); + + RectangleF lineRect = new( + (float)left, + (float)top, + (float)Math.Abs(right - left), + (float)Math.Abs(bottom - top)); + + StringBuilder lineText = new(); + ocrLine.GetTextFromOcrLine(true, lineText, shouldCorrectToLatin); + + WordBorderInfo info = new() + { + BorderRect = lineRect, + Word = lineText.ToString().Trim(), + ResultRowID = 0, + ResultColumnID = 0 + }; + + infos.Add(info); + } + + return infos; + } + + public static void GetTextFromOcrLine( + this IOcrLine ocrLine, + bool isSpaceJoiningOCRLang, + StringBuilder text, + bool shouldCorrectToLatin = true) + { + // (when OCR language is zh or ja) + // matches words in a space-joining language, which contains: + // - one letter that is not in "other letters" (CJK characters are "other letters") + // - one number digit + // - any words longer than one character + // Chinese and Japanese characters are single-character words + // when a word is one punctuation/symbol, join it without spaces + + if (isSpaceJoiningOCRLang) + { + text.AppendLine(ocrLine.Text); + + if (SettingsAccess.Current.CorrectErrors) + text.TryFixEveryWordLetterNumberErrors(); + } + else + { + // For CJK languages, filter out likely furigana (small ruby-text + // characters above the main text) before merging the words. This is + // opt-in via the RemoveFurigana setting. + IEnumerable words = SettingsAccess.Current.RemoveFurigana + ? FilterFurigana([.. ocrLine.Words]) + : ocrLine.Words; + + bool isFirstWord = true; + bool isPrevWordSpaceJoining = false; + + foreach (IOcrWord ocrWord in words) + { + string wordString = ocrWord.Text; + + bool isThisWordSpaceJoining = _cachedSpaceJoiningWordRegex.IsMatch(wordString); + + if (SettingsAccess.Current.CorrectErrors) + wordString = wordString.TryFixNumberLetterErrors(); + + if (isFirstWord || (!isThisWordSpaceJoining && !isPrevWordSpaceJoining)) + _ = text.Append(wordString); + else + _ = text.Append(' ').Append(wordString); + + isFirstWord = false; + isPrevWordSpaceJoining = isThisWordSpaceJoining; + } + } + + if (SettingsAccess.Current.CorrectToLatin && shouldCorrectToLatin) + text.ReplaceGreekOrCyrillicWithLatin(); + } + + /// + /// Removes words that are likely furigana: small ruby-text characters + /// rendered above the main text in Japanese. A word is treated as furigana + /// when it is noticeably shorter than the line's median word height and sits + /// directly above a larger word that overlaps it horizontally. + /// + internal static List FilterFurigana(List words) + { + if (words.Count == 0) + return words; + + // Furigana is typically around half the height of the main text. + List heights = [.. words.Select(w => w.BoundingBox.Height).OrderBy(h => h)]; + double medianHeight = heights[heights.Count / 2]; + double furiganaThreshold = medianHeight * 0.6; + + List filteredWords = []; + + for (int i = 0; i < words.Count; i++) + { + IOcrWord word = words[i]; + bool isProbablyFurigana = false; + + if (word.BoundingBox.Height < furiganaThreshold) + { + // Only treat it as furigana when a larger word sits below it and + // overlaps horizontally (i.e. the kanji it annotates). + for (int j = 0; j < words.Count; j++) + { + if (i == j) + continue; + + IOcrWord otherWord = words[j]; + + bool isBelow = otherWord.BoundingBox.Top > word.BoundingBox.Bottom; + bool overlapsHorizontally = !(otherWord.BoundingBox.Right < word.BoundingBox.Left + || otherWord.BoundingBox.Left > word.BoundingBox.Right); + bool isLarger = otherWord.BoundingBox.Height > furiganaThreshold; + + if (isBelow && overlapsHorizontally && isLarger) + { + isProbablyFurigana = word.Text.Length <= 2; + break; + } + } + } + + if (!isProbablyFurigana) + filteredWords.Add(word); + } + + // If everything was filtered, fall back to the original words to avoid + // dropping the whole line. + return filteredWords.Count > 0 ? filteredWords : words; + } + + internal readonly record struct PositionedOcrLine(int LineNumber, string Text, Windows.Foundation.Rect BoundingBox); + + internal sealed class GroupedOcrLines(IReadOnlyList lines, Windows.Foundation.Rect boundingBox) + { + public Windows.Foundation.Rect BoundingBox { get; } = boundingBox; + + public IReadOnlyList Lines { get; } = lines; + + public int StartingLineNumber => Lines.Count == 0 ? 0 : Lines[0].LineNumber; + + public string DisplayText => string.Join(Environment.NewLine, Lines.Select(static line => line.Text.MakeStringSingleLine())); + + public string SingleLineText => string.Join(" ", Lines.Select(static line => line.Text.MakeStringSingleLine()).Where(static text => !string.IsNullOrWhiteSpace(text))); + } + + internal static string BuildTextFromOcrLines(ILanguage language, IOcrLinesWords ocrResult) + { + StringBuilder text = new(); + + bool isSpaceJoiningOCRLang = language.IsSpaceJoining(); + IOcrLine[] lines = ocrResult.Lines; + + if (ShouldUseParagraphDetection(isSpaceJoiningOCRLang) && lines.Length > 0) + { + List groupedLines = + [ + .. GroupWrappedParagraphLines( + [.. lines.Select((line, index) => new PositionedOcrLine(index, line.Text, line.BoundingBox))]) + ]; + + for (int i = 0; i < groupedLines.Count; i++) + { + if (i > 0) + text.AppendLine(); + + text.Append(groupedLines[i].SingleLineText); + } + } + else + { + // Windows OCR returns CJK lines - especially furigana ruby lines and + // stray fragments - in an order that does not follow the page's reading + // flow, so re-sort by geometry (top-to-bottom, then left-to-right) + // before joining. Space-joining languages keep the engine order because + // paragraph detection above already handles their layout. + IReadOnlyList orderedLines = isSpaceJoiningOCRLang + ? lines + : OrderLinesForReadingFlow(lines); + + // Windows OCR emits furigana (Japanese ruby readings) as their own + // short lines sitting directly above the kanji they annotate, so the + // word-level filter above never catches them. Drop those whole lines + // when furigana removal is enabled. + if (!isSpaceJoiningOCRLang && SettingsAccess.Current.RemoveFurigana) + orderedLines = FilterFuriganaLines(orderedLines); + + foreach (IOcrLine ocrLine in orderedLines) + ocrLine.GetTextFromOcrLine(isSpaceJoiningOCRLang, text, language.IsLatinBased()); + } + + if (language.IsRightToLeft()) + text.ReverseWordsForRightToLeft(); + + return text.ToString(); + } + + /// + /// Re-orders OCR lines into natural reading flow: groups lines that share a + /// horizontal row (their vertical extents overlap), orders rows top-to-bottom, + /// and orders the lines within each row left-to-right. Windows OCR frequently + /// returns CJK lines out of order (furigana above kanji, trailing fragments), + /// which scrambles the concatenated text without this pass. + /// + internal static IReadOnlyList OrderLinesForReadingFlow(IReadOnlyList lines) + { + if (lines.Count <= 1) + return lines; + + // Stable sort by the top edge so rows are discovered top-to-bottom. + List byTop = [.. lines.OrderBy(line => line.BoundingBox.Top)]; + + List> rows = []; + double currentRowTop = 0; + double currentRowBottom = 0; + + foreach (IOcrLine line in byTop) + { + Windows.Foundation.Rect box = line.BoundingBox; + + if (rows.Count > 0) + { + double overlap = Math.Min(currentRowBottom, box.Bottom) - Math.Max(currentRowTop, box.Top); + double minHeight = Math.Min(currentRowBottom - currentRowTop, box.Height); + + // A line joins the current row when it overlaps the row's vertical + // band by more than half of the shorter of the two heights. + if (minHeight > 0 && overlap > minHeight * 0.5) + { + rows[^1].Add(line); + currentRowTop = Math.Min(currentRowTop, box.Top); + currentRowBottom = Math.Max(currentRowBottom, box.Bottom); + continue; + } + } + + rows.Add([line]); + currentRowTop = box.Top; + currentRowBottom = box.Bottom; + } + + List ordered = []; + foreach (List row in rows) + ordered.AddRange(row.OrderBy(line => line.BoundingBox.Left)); + + return ordered; + } + + /// + /// Removes whole OCR lines that are likely furigana: short ruby-reading lines + /// that sit directly above a substantially taller line overlapping them + /// horizontally (the kanji they annotate). Windows OCR returns furigana as + /// their own lines, so this complements the word-level . + /// The heuristic is intentionally conservative and geometry-only; it can miss + /// mis-detected readings and is offered as an opt-in, experimental setting. + /// + internal static IReadOnlyList FilterFuriganaLines(IReadOnlyList lines) + { + if (lines.Count < 2) + return lines; + + List kept = []; + + for (int i = 0; i < lines.Count; i++) + { + Windows.Foundation.Rect box = lines[i].BoundingBox; + bool isFurigana = false; + + for (int j = 0; j < lines.Count; j++) + { + if (i == j) + continue; + + Windows.Foundation.Rect other = lines[j].BoundingBox; + + bool isBelow = other.Top >= box.Bottom; + bool overlapsHorizontally = !(other.Right < box.Left || other.Left > box.Right); + // The annotated kanji is markedly taller than its reading. + bool isSubstantiallyTaller = other.Height > box.Height * 1.4; + // Ruby text hugs the top of its character; a large vertical gap + // means these are separate lines of body text, not a reading. + bool isCloseAbove = other.Top - box.Bottom < box.Height; + + if (isBelow && overlapsHorizontally && isSubstantiallyTaller && isCloseAbove) + { + isFurigana = true; + break; + } + } + + if (!isFurigana) + kept.Add(lines[i]); + } + + // Never drop everything - fall back to the input if the heuristic would + // erase the whole result. + return kept.Count > 0 ? kept : lines; + } + + internal static bool ShouldUseParagraphDetection(bool isSpaceJoiningLanguage, bool isTableMode = false) + { + return SettingsAccess.Current.ParagraphDetection && isSpaceJoiningLanguage && !isTableMode; + } + + internal static List GroupWrappedParagraphLines(IReadOnlyList lines) + { + List groupedLines = []; + + if (lines.Count == 0) + return groupedLines; + + List currentGroup = [lines[0]]; + Windows.Foundation.Rect currentBounds = lines[0].BoundingBox; + + for (int i = 1; i < lines.Count; i++) + { + PositionedOcrLine previousLine = currentGroup[^1]; + PositionedOcrLine currentLine = lines[i]; + + if (IsWrappedParagraph( + previousLine.BoundingBox.Y, + previousLine.BoundingBox.Height, + currentLine.BoundingBox.Y, + currentLine.BoundingBox.Height)) + { + currentGroup.Add(currentLine); + currentBounds = UnionRectangles(currentBounds, currentLine.BoundingBox); + continue; + } + + groupedLines.Add(new GroupedOcrLines([.. currentGroup], currentBounds)); + currentGroup = [currentLine]; + currentBounds = currentLine.BoundingBox; + } + + groupedLines.Add(new GroupedOcrLines([.. currentGroup], currentBounds)); + return groupedLines; + } + + private static Windows.Foundation.Rect UnionRectangles(Windows.Foundation.Rect current, Windows.Foundation.Rect next) + { + if (current.IsEmpty) + return next; + + if (next.IsEmpty) + return current; + + double left = Math.Min(current.X, next.X); + double top = Math.Min(current.Y, next.Y); + double right = Math.Max(current.X + current.Width, next.X + next.Width); + double bottom = Math.Max(current.Y + current.Height, next.Y + next.Height); + return new Windows.Foundation.Rect(left, top, right - left, bottom - top); + } + + /// + /// Determines whether two consecutive lines belong to the same wrapped paragraph + /// by comparing the vertical gap between them relative to the average line height. + /// Returns true if the lines should be joined with a space (same paragraph, wrapped), + /// false if they should be separated by a newline (different paragraphs). + /// + internal static bool IsWrappedLine(IOcrLine currentLine, IOcrLine nextLine) + { + if (currentLine.BoundingBox.IsEmpty || nextLine.BoundingBox.IsEmpty) + return false; + + return IsWrappedParagraph( + currentLine.BoundingBox.Y, + currentLine.BoundingBox.Height, + nextLine.BoundingBox.Y, + nextLine.BoundingBox.Height); + } + + /// + /// Core paragraph-wrap heuristic: returns true when the vertical gap between two + /// lines is small enough (less than 60 % of the average line height) that they + /// belong to the same wrapped paragraph, and their heights are similar (ratio ≤ 1.5). + /// Works for any coordinate space — ratios are scale-invariant. + /// + internal static bool IsWrappedParagraph( + double currentTop, double currentHeight, + double nextTop, double nextHeight) + { + if (currentHeight <= 0 || nextHeight <= 0) + return false; + + // Lines with significantly different heights are likely different content blocks + double minHeight = Math.Min(currentHeight, nextHeight); + double maxHeight = Math.Max(currentHeight, nextHeight); + if (maxHeight / minHeight > 1.5) + return false; + + // Consecutive OCR entries must advance to a distinct visual row. Without + // this guard, duplicate or horizontally split entries on the same row have + // a negative gap and are incorrectly merged into a one-line-tall paragraph. + if (nextTop - currentTop < minHeight * 0.5) + return false; + + // If the vertical gap between line bounding boxes is less than 0.6× the average line + // height, the lines are part of the same paragraph (normal line spacing); otherwise + // the extra whitespace signals a paragraph break. + double gap = nextTop - (currentTop + currentHeight); + double avgLineHeight = (currentHeight + nextHeight) / 2.0; + return gap < avgLineHeight * 0.6; + } + + public static string GetStringFromOcrOutputs(List outputs) + { + StringBuilder text = new(); + + foreach (OcrOutput output in outputs) + { + output.CleanOutput(); + + if (!string.IsNullOrWhiteSpace(output.CleanedOutput)) + text.Append(output.CleanedOutput); + else if (!string.IsNullOrWhiteSpace(output.RawOutput)) + text.Append(output.RawOutput); + } + + return text.ToString(); + } + + [GeneratedRegex(@"(^[\p{L}-[\p{Lo}]]|\p{Nd}$)|.{2,}")] + private static partial Regex SpaceJoiningWordRegex(); +} diff --git a/Text-Grab/App.xaml.cs b/Text-Grab/App.xaml.cs index 33e2acba..9af38c47 100644 --- a/Text-Grab/App.xaml.cs +++ b/Text-Grab/App.xaml.cs @@ -469,7 +469,7 @@ private static async Task GrabTextFromFileAsync(string path) { try { - string ocrText = await OcrUtilities.OcrAbsoluteFilePathAsync( + string ocrText = await OcrSourceUtilities.OcrAbsoluteFilePathAsync( path, LanguageUtilities.GetOCRLanguage()); OutputUtilities.HandleTextFromOcr(ocrText, isSingleLine: false, isTable: false); } diff --git a/Text-Grab/Controls/NotifyIconWindow.xaml.cs b/Text-Grab/Controls/NotifyIconWindow.xaml.cs index 85d9294f..d5341046 100644 --- a/Text-Grab/Controls/NotifyIconWindow.xaml.cs +++ b/Text-Grab/Controls/NotifyIconWindow.xaml.cs @@ -131,7 +131,7 @@ private void FullscreenGrabMenuItem_Click(object sender, RoutedEventArgs e) private async void PreviousRegionMenuItem_Click(object sender, RoutedEventArgs e) { - await OcrUtilities.GetTextFromPreviousFullscreenRegion(); + await OcrSourceUtilities.GetTextFromPreviousFullscreenRegion(); } private void LookupMenuItem_Click(object sender, RoutedEventArgs e) diff --git a/Text-Grab/Extensions/LanguageRtlExtensions.cs b/Text-Grab/Extensions/LanguageRtlExtensions.cs deleted file mode 100644 index f1b43776..00000000 --- a/Text-Grab/Extensions/LanguageRtlExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Globalization; -using System.Windows.Markup; -using Text_Grab.Interfaces; -using Text_Grab.Models; -using Windows.Globalization; - -namespace Text_Grab; - -/// -/// The WPF-bound half of what used to be Extensions/LanguageExtensions.cs. Split out because -/// comes from PresentationCore, so it cannot cross into -/// Text-Grab.Core.Windows (see docs/Core-Split-Plan.md, batch 3d). The portable members -/// (IsSpaceJoining, IsLatinBased, AsLanguage, AsILanguage) moved to Core.Windows keeping the -/// original LanguageExtensions type name so existing call sites resolve unchanged; these two -/// overloads stay here under a distinct name to avoid a same-named-extension-method ambiguity -/// (CS0121) between the two assemblies. -/// -public static class LanguageRtlExtensions -{ - public static bool IsRightToLeft(this Language language) - { - XmlLanguage lang = XmlLanguage.GetLanguage(language.LanguageTag); - CultureInfo culture = lang.GetEquivalentCulture(); - return culture.TextInfo.IsRightToLeft; - } - - public static bool IsRightToLeft(this ILanguage selectedLanguage) - { - if (selectedLanguage is GlobalLang language) - return language.OriginalLanguage.IsRightToLeft(); - - // For other language types, use the LayoutDirection property - return selectedLanguage.LayoutDirection == LanguageLayoutDirection.Rtl; - } -} diff --git a/Text-Grab/Utilities/FileOpenUtilities.cs b/Text-Grab/Utilities/FileOpenUtilities.cs index 00cd3ed3..cbca7948 100644 --- a/Text-Grab/Utilities/FileOpenUtilities.cs +++ b/Text-Grab/Utilities/FileOpenUtilities.cs @@ -20,7 +20,7 @@ public class FileOpenUtilities { try { - stringBuilder.Append(await OcrUtilities.OcrAbsoluteFilePathAsync(pathOfFileToOpen, language)); + stringBuilder.Append(await OcrSourceUtilities.OcrAbsoluteFilePathAsync(pathOfFileToOpen, language)); } catch (Exception) { diff --git a/Text-Grab/Utilities/GrabTemplateExecutor.cs b/Text-Grab/Utilities/GrabTemplateExecutor.cs index d5ef78da..6733ce36 100644 --- a/Text-Grab/Utilities/GrabTemplateExecutor.cs +++ b/Text-Grab/Utilities/GrabTemplateExecutor.cs @@ -98,7 +98,7 @@ public static async Task ExecuteTemplateAsync( { try { - fullAreaText = await OcrUtilities.GetTextFromAbsoluteRectAsync(captureRegion, resolvedLanguage); + fullAreaText = await OcrSourceUtilities.GetTextFromAbsoluteRectAsync(captureRegion, resolvedLanguage); } catch (Exception) { @@ -166,7 +166,7 @@ public static async Task ExecuteTemplateOnBitmapAsync( using Bitmap regionBitmap = bitmap.Clone( new Rectangle(x, y, width, height), bitmap.PixelFormat); string regionText = OcrUtilities.GetStringFromOcrOutputs( - await OcrUtilities.GetTextFromImageAsync(regionBitmap, resolvedLanguage)); + await OcrSourceUtilities.GetTextFromImageAsync(regionBitmap, resolvedLanguage)); regionResults[region.RegionNumber] = string.IsNullOrWhiteSpace(regionText) ? region.DefaultValue : regionText.Trim(); @@ -194,7 +194,7 @@ public static async Task ExecuteTemplateOnBitmapAsync( try { fullAreaText = OcrUtilities.GetStringFromOcrOutputs( - await OcrUtilities.GetTextFromImageAsync(bitmap, resolvedLanguage)); + await OcrSourceUtilities.GetTextFromImageAsync(bitmap, resolvedLanguage)); } catch (Exception) { @@ -540,7 +540,7 @@ private static async Task> OcrAllRegionsAsync( try { // GetTextFromAbsoluteRectAsync uses absolute screen coordinates - string regionText = await OcrUtilities.GetTextFromAbsoluteRectAsync(absoluteRegionRect, language); + string regionText = await OcrSourceUtilities.GetTextFromAbsoluteRectAsync(absoluteRegionRect, language); // Use default value when OCR returns nothing results[region.RegionNumber] = string.IsNullOrWhiteSpace(regionText) ? region.DefaultValue diff --git a/Text-Grab/Utilities/NotifyIconUtilities.cs b/Text-Grab/Utilities/NotifyIconUtilities.cs index eadf9cac..5bd371b8 100644 --- a/Text-Grab/Utilities/NotifyIconUtilities.cs +++ b/Text-Grab/Utilities/NotifyIconUtilities.cs @@ -132,7 +132,7 @@ private static void HotKeyManager_HotKeyPressed(object? sender, HotKeyEventArgs case ShortcutKeyActions.PreviousRegionGrab: System.Windows.Application.Current.Dispatcher.Invoke(new Action(() => { - OcrUtilities.GetCopyTextFromPreviousRegion(); + OcrSourceUtilities.GetCopyTextFromPreviousRegion(); })); break; case ShortcutKeyActions.PreviousEditWindow: diff --git a/Text-Grab/Utilities/OcrUtilities.cs b/Text-Grab/Utilities/OcrSourceUtilities.cs similarity index 55% rename from Text-Grab/Utilities/OcrUtilities.cs rename to Text-Grab/Utilities/OcrSourceUtilities.cs index 27c803d7..7acbdcde 100644 --- a/Text-Grab/Utilities/OcrUtilities.cs +++ b/Text-Grab/Utilities/OcrSourceUtilities.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -26,13 +25,25 @@ namespace Text_Grab.Utilities; -public static partial class OcrUtilities +/// +/// OCR entry points bound to app-side image sources: screen regions, windows, BitmapSource, +/// files and streams - plus engine dispatch. +/// +/// The app-coupled half of what used to be OcrUtilities (batch 4c of the Core split). The text +/// assembly it feeds - line and word joining, furigana filtering, paragraph grouping - moved to +/// Text-Grab.Core.Windows keeping the OcrUtilities name, so the call sites there resolve +/// unchanged; this half took the new name instead. +/// +/// What holds it here: System.Windows.Window and BitmapSource throughout, and engine dispatch +/// via WindowsAiUtilities and LanguageUtilities, neither of which has moved (WindowsAiUtilities +/// is blocked on SoftwareBitmapExtensions in wave 5a). LoadBitmapFromFile stays deferred on its +/// own account - it builds a WPF BitmapImage to apply EXIF rotation, and decoupling it means a +/// GDI+/WIC rewrite, which takes OcrAbsoluteFilePathAsync and OcrFile with it. +/// +public static class OcrSourceUtilities { private static readonly Settings DefaultSettings = AppUtilities.TextGrabSettings; - // Cache the SpaceJoiningWordRegex to avoid creating it on every method call - private static readonly Regex _cachedSpaceJoiningWordRegex = SpaceJoiningWordRegex(); - private static bool IsUiAutomationLanguage(ILanguage language) => language is UiAutomationLang; private static bool IsWindowsAiDescriptionLanguage(ILanguage language) => language is WindowsAiDescriptionLang; @@ -50,157 +61,6 @@ private static ILanguage GetCompatibleOcrLanguage(ILanguage language) return handle == IntPtr.Zero ? null : [handle]; } - // Moved from ResultTable when that class moved to Text-Grab.Core (batch 4d of the Core - // split): this stays app-side because it calls GetTextFromOcrLine below, an app-only - // extension method that reads settings and is itself blocked pending Wave 4c. - public static List ParseOcrResultIntoWordBorderInfos( - IOcrLinesWords ocrResult, - bool shouldCorrectToLatin = true) - { - List infos = []; - - foreach (IOcrLine ocrLine in ocrResult.Lines) - { - double top = ocrLine.Words.Select(x => x.BoundingBox.Top).Min(); - double bottom = ocrLine.Words.Select(x => x.BoundingBox.Bottom).Max(); - double left = ocrLine.Words.Select(x => x.BoundingBox.Left).Min(); - double right = ocrLine.Words.Select(x => x.BoundingBox.Right).Max(); - - RectangleF lineRect = new( - (float)left, - (float)top, - (float)Math.Abs(right - left), - (float)Math.Abs(bottom - top)); - - StringBuilder lineText = new(); - ocrLine.GetTextFromOcrLine(true, lineText, shouldCorrectToLatin); - - WordBorderInfo info = new() - { - BorderRect = lineRect, - Word = lineText.ToString().Trim(), - ResultRowID = 0, - ResultColumnID = 0 - }; - - infos.Add(info); - } - - return infos; - } - - public static void GetTextFromOcrLine( - this IOcrLine ocrLine, - bool isSpaceJoiningOCRLang, - StringBuilder text, - bool shouldCorrectToLatin = true) - { - // (when OCR language is zh or ja) - // matches words in a space-joining language, which contains: - // - one letter that is not in "other letters" (CJK characters are "other letters") - // - one number digit - // - any words longer than one character - // Chinese and Japanese characters are single-character words - // when a word is one punctuation/symbol, join it without spaces - - if (isSpaceJoiningOCRLang) - { - text.AppendLine(ocrLine.Text); - - if (DefaultSettings.CorrectErrors) - text.TryFixEveryWordLetterNumberErrors(); - } - else - { - // For CJK languages, filter out likely furigana (small ruby-text - // characters above the main text) before merging the words. This is - // opt-in via the RemoveFurigana setting. - IEnumerable words = DefaultSettings.RemoveFurigana - ? FilterFurigana([.. ocrLine.Words]) - : ocrLine.Words; - - bool isFirstWord = true; - bool isPrevWordSpaceJoining = false; - - foreach (IOcrWord ocrWord in words) - { - string wordString = ocrWord.Text; - - bool isThisWordSpaceJoining = _cachedSpaceJoiningWordRegex.IsMatch(wordString); - - if (DefaultSettings.CorrectErrors) - wordString = wordString.TryFixNumberLetterErrors(); - - if (isFirstWord || (!isThisWordSpaceJoining && !isPrevWordSpaceJoining)) - _ = text.Append(wordString); - else - _ = text.Append(' ').Append(wordString); - - isFirstWord = false; - isPrevWordSpaceJoining = isThisWordSpaceJoining; - } - } - - if (DefaultSettings.CorrectToLatin && shouldCorrectToLatin) - text.ReplaceGreekOrCyrillicWithLatin(); - } - - /// - /// Removes words that are likely furigana: small ruby-text characters - /// rendered above the main text in Japanese. A word is treated as furigana - /// when it is noticeably shorter than the line's median word height and sits - /// directly above a larger word that overlaps it horizontally. - /// - internal static List FilterFurigana(List words) - { - if (words.Count == 0) - return words; - - // Furigana is typically around half the height of the main text. - List heights = [.. words.Select(w => w.BoundingBox.Height).OrderBy(h => h)]; - double medianHeight = heights[heights.Count / 2]; - double furiganaThreshold = medianHeight * 0.6; - - List filteredWords = []; - - for (int i = 0; i < words.Count; i++) - { - IOcrWord word = words[i]; - bool isProbablyFurigana = false; - - if (word.BoundingBox.Height < furiganaThreshold) - { - // Only treat it as furigana when a larger word sits below it and - // overlaps horizontally (i.e. the kanji it annotates). - for (int j = 0; j < words.Count; j++) - { - if (i == j) - continue; - - IOcrWord otherWord = words[j]; - - bool isBelow = otherWord.BoundingBox.Top > word.BoundingBox.Bottom; - bool overlapsHorizontally = !(otherWord.BoundingBox.Right < word.BoundingBox.Left - || otherWord.BoundingBox.Left > word.BoundingBox.Right); - bool isLarger = otherWord.BoundingBox.Height > furiganaThreshold; - - if (isBelow && overlapsHorizontally && isLarger) - { - isProbablyFurigana = word.Text.Length <= 2; - break; - } - } - } - - if (!isProbablyFurigana) - filteredWords.Add(word); - } - - // If everything was filtered, fall back to the original words to avoid - // dropping the whole line. - return filteredWords.Count > 0 ? filteredWords : words; - } - public static async Task GetTextFromAbsoluteRectAsync( Rect rect, ILanguage language, @@ -218,7 +78,7 @@ public static async Task GetTextFromAbsoluteRectAsync( Bitmap bmp = preCapturedBitmap ?? ImageMethods.GetRegionOfScreenAsBitmap(rect.AsRectangle()); - return GetStringFromOcrOutputs(await GetTextFromImageAsync(bmp, language)); + return OcrUtilities.GetStringFromOcrOutputs(await GetTextFromImageAsync(bmp, language)); } public static async Task GetRegionsTextAsync(Window passedWindow, Rectangle selectedRegion, ILanguage language) @@ -247,7 +107,7 @@ public static async Task GetRegionsTextAsTableAsync(Window passedWindow, IOcrLinesWords ocrResult = await GetOcrResultFromImageAsync(scaledBitmap, compatibleLanguage); // New model-only flow - List wordBorderInfos = ParseOcrResultIntoWordBorderInfos( + List wordBorderInfos = OcrUtilities.ParseOcrResultIntoWordBorderInfos( ocrResult, compatibleLanguage.IsLatinBased()); @@ -277,7 +137,7 @@ public static async Task GetTextFromBitmapAsync(Bitmap bitmap, ILanguage language = GetCompatibleOcrLanguage(language); } - return GetStringFromOcrOutputs(await GetTextFromImageAsync(bitmap, language)); + return OcrUtilities.GetStringFromOcrOutputs(await GetTextFromImageAsync(bitmap, language)); } public static async Task GetTextFromBitmapSourceAsync(BitmapSource bitmapSource, ILanguage language) @@ -293,7 +153,7 @@ public static async Task GetTextFromBitmapAsTableAsync(Bitmap bitmap, IL using Bitmap scaledBitmap = ImageMethods.ScaleBitmapUniform(bitmap, scale); IOcrLinesWords ocrResult = await GetOcrResultFromImageAsync(scaledBitmap, compatibleLanguage); - List wordBorderInfos = ParseOcrResultIntoWordBorderInfos( + List wordBorderInfos = OcrUtilities.ParseOcrResultIntoWordBorderInfos( ocrResult, compatibleLanguage.IsLatinBased()); @@ -520,7 +380,7 @@ public static async Task> GetTextFromRandomAccessStream(IRandomA public static async Task> GetTextFromWinAiAsync(Bitmap bitmap, WindowsAiLang language) { - if (ShouldUseParagraphDetection(language.IsSpaceJoining())) + if (OcrUtilities.ShouldUseParagraphDetection(language.IsSpaceJoining())) { WinAiOcrLinesWords? ocrResult = await WindowsAiUtilities.GetOcrResultAsync(bitmap); if (ocrResult is not null) @@ -591,7 +451,7 @@ public static async Task> GetTextFromImageAsync(Bitmap bitmap, I GlobalLang ocrLanguageFromILang = language as GlobalLang ?? new GlobalLang("en-US"); double scale = await GetIdealScaleFactorForOcrAsync(bitmap, ocrLanguageFromILang); using Bitmap scaledBitmap = ImageMethods.ScaleBitmapUniform(bitmap, scale); - IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(scaledBitmap, ocrLanguageFromILang); + IOcrLinesWords ocrResult = await OcrSourceUtilities.GetOcrResultFromImageAsync(scaledBitmap, ocrLanguageFromILang); OcrOutput paragraphsOutput = GetTextFromOcrResult(ocrLanguageFromILang, new Bitmap(scaledBitmap), ocrResult); outputs.Add(paragraphsOutput); } @@ -607,304 +467,13 @@ private static OcrOutput GetTextFromOcrResult(ILanguage language, Bitmap? scaled OcrOutput paragraphsOutput = new() { Kind = OcrOutputKind.Paragraph, - RawOutput = BuildTextFromOcrLines(language, ocrResult), + RawOutput = OcrUtilities.BuildTextFromOcrLines(language, ocrResult), Language = language, SourceBitmap = scaledBitmap, }; return paragraphsOutput; } - internal readonly record struct PositionedOcrLine(int LineNumber, string Text, Windows.Foundation.Rect BoundingBox); - - internal sealed class GroupedOcrLines(IReadOnlyList lines, Windows.Foundation.Rect boundingBox) - { - public Windows.Foundation.Rect BoundingBox { get; } = boundingBox; - - public IReadOnlyList Lines { get; } = lines; - - public int StartingLineNumber => Lines.Count == 0 ? 0 : Lines[0].LineNumber; - - public string DisplayText => string.Join(Environment.NewLine, Lines.Select(static line => line.Text.MakeStringSingleLine())); - - public string SingleLineText => string.Join(" ", Lines.Select(static line => line.Text.MakeStringSingleLine()).Where(static text => !string.IsNullOrWhiteSpace(text))); - } - - internal static string BuildTextFromOcrLines(ILanguage language, IOcrLinesWords ocrResult) - { - StringBuilder text = new(); - - bool isSpaceJoiningOCRLang = language.IsSpaceJoining(); - IOcrLine[] lines = ocrResult.Lines; - - if (ShouldUseParagraphDetection(isSpaceJoiningOCRLang) && lines.Length > 0) - { - List groupedLines = - [ - .. GroupWrappedParagraphLines( - [.. lines.Select((line, index) => new PositionedOcrLine(index, line.Text, line.BoundingBox))]) - ]; - - for (int i = 0; i < groupedLines.Count; i++) - { - if (i > 0) - text.AppendLine(); - - text.Append(groupedLines[i].SingleLineText); - } - } - else - { - // Windows OCR returns CJK lines - especially furigana ruby lines and - // stray fragments - in an order that does not follow the page's reading - // flow, so re-sort by geometry (top-to-bottom, then left-to-right) - // before joining. Space-joining languages keep the engine order because - // paragraph detection above already handles their layout. - IReadOnlyList orderedLines = isSpaceJoiningOCRLang - ? lines - : OrderLinesForReadingFlow(lines); - - // Windows OCR emits furigana (Japanese ruby readings) as their own - // short lines sitting directly above the kanji they annotate, so the - // word-level filter above never catches them. Drop those whole lines - // when furigana removal is enabled. - if (!isSpaceJoiningOCRLang && DefaultSettings.RemoveFurigana) - orderedLines = FilterFuriganaLines(orderedLines); - - foreach (IOcrLine ocrLine in orderedLines) - ocrLine.GetTextFromOcrLine(isSpaceJoiningOCRLang, text, language.IsLatinBased()); - } - - if (language.IsRightToLeft()) - text.ReverseWordsForRightToLeft(); - - return text.ToString(); - } - - /// - /// Re-orders OCR lines into natural reading flow: groups lines that share a - /// horizontal row (their vertical extents overlap), orders rows top-to-bottom, - /// and orders the lines within each row left-to-right. Windows OCR frequently - /// returns CJK lines out of order (furigana above kanji, trailing fragments), - /// which scrambles the concatenated text without this pass. - /// - internal static IReadOnlyList OrderLinesForReadingFlow(IReadOnlyList lines) - { - if (lines.Count <= 1) - return lines; - - // Stable sort by the top edge so rows are discovered top-to-bottom. - List byTop = [.. lines.OrderBy(line => line.BoundingBox.Top)]; - - List> rows = []; - double currentRowTop = 0; - double currentRowBottom = 0; - - foreach (IOcrLine line in byTop) - { - Windows.Foundation.Rect box = line.BoundingBox; - - if (rows.Count > 0) - { - double overlap = Math.Min(currentRowBottom, box.Bottom) - Math.Max(currentRowTop, box.Top); - double minHeight = Math.Min(currentRowBottom - currentRowTop, box.Height); - - // A line joins the current row when it overlaps the row's vertical - // band by more than half of the shorter of the two heights. - if (minHeight > 0 && overlap > minHeight * 0.5) - { - rows[^1].Add(line); - currentRowTop = Math.Min(currentRowTop, box.Top); - currentRowBottom = Math.Max(currentRowBottom, box.Bottom); - continue; - } - } - - rows.Add([line]); - currentRowTop = box.Top; - currentRowBottom = box.Bottom; - } - - List ordered = []; - foreach (List row in rows) - ordered.AddRange(row.OrderBy(line => line.BoundingBox.Left)); - - return ordered; - } - - /// - /// Removes whole OCR lines that are likely furigana: short ruby-reading lines - /// that sit directly above a substantially taller line overlapping them - /// horizontally (the kanji they annotate). Windows OCR returns furigana as - /// their own lines, so this complements the word-level . - /// The heuristic is intentionally conservative and geometry-only; it can miss - /// mis-detected readings and is offered as an opt-in, experimental setting. - /// - internal static IReadOnlyList FilterFuriganaLines(IReadOnlyList lines) - { - if (lines.Count < 2) - return lines; - - List kept = []; - - for (int i = 0; i < lines.Count; i++) - { - Windows.Foundation.Rect box = lines[i].BoundingBox; - bool isFurigana = false; - - for (int j = 0; j < lines.Count; j++) - { - if (i == j) - continue; - - Windows.Foundation.Rect other = lines[j].BoundingBox; - - bool isBelow = other.Top >= box.Bottom; - bool overlapsHorizontally = !(other.Right < box.Left || other.Left > box.Right); - // The annotated kanji is markedly taller than its reading. - bool isSubstantiallyTaller = other.Height > box.Height * 1.4; - // Ruby text hugs the top of its character; a large vertical gap - // means these are separate lines of body text, not a reading. - bool isCloseAbove = other.Top - box.Bottom < box.Height; - - if (isBelow && overlapsHorizontally && isSubstantiallyTaller && isCloseAbove) - { - isFurigana = true; - break; - } - } - - if (!isFurigana) - kept.Add(lines[i]); - } - - // Never drop everything - fall back to the input if the heuristic would - // erase the whole result. - return kept.Count > 0 ? kept : lines; - } - - internal static bool ShouldUseParagraphDetection(bool isSpaceJoiningLanguage, bool isTableMode = false) - { - return DefaultSettings.ParagraphDetection && isSpaceJoiningLanguage && !isTableMode; - } - - internal static List GroupWrappedParagraphLines(IReadOnlyList lines) - { - List groupedLines = []; - - if (lines.Count == 0) - return groupedLines; - - List currentGroup = [lines[0]]; - Windows.Foundation.Rect currentBounds = lines[0].BoundingBox; - - for (int i = 1; i < lines.Count; i++) - { - PositionedOcrLine previousLine = currentGroup[^1]; - PositionedOcrLine currentLine = lines[i]; - - if (IsWrappedParagraph( - previousLine.BoundingBox.Y, - previousLine.BoundingBox.Height, - currentLine.BoundingBox.Y, - currentLine.BoundingBox.Height)) - { - currentGroup.Add(currentLine); - currentBounds = UnionRectangles(currentBounds, currentLine.BoundingBox); - continue; - } - - groupedLines.Add(new GroupedOcrLines([.. currentGroup], currentBounds)); - currentGroup = [currentLine]; - currentBounds = currentLine.BoundingBox; - } - - groupedLines.Add(new GroupedOcrLines([.. currentGroup], currentBounds)); - return groupedLines; - } - - private static Windows.Foundation.Rect UnionRectangles(Windows.Foundation.Rect current, Windows.Foundation.Rect next) - { - if (current.IsEmpty) - return next; - - if (next.IsEmpty) - return current; - - double left = Math.Min(current.X, next.X); - double top = Math.Min(current.Y, next.Y); - double right = Math.Max(current.X + current.Width, next.X + next.Width); - double bottom = Math.Max(current.Y + current.Height, next.Y + next.Height); - return new Windows.Foundation.Rect(left, top, right - left, bottom - top); - } - - /// - /// Determines whether two consecutive lines belong to the same wrapped paragraph - /// by comparing the vertical gap between them relative to the average line height. - /// Returns true if the lines should be joined with a space (same paragraph, wrapped), - /// false if they should be separated by a newline (different paragraphs). - /// - internal static bool IsWrappedLine(IOcrLine currentLine, IOcrLine nextLine) - { - if (currentLine.BoundingBox.IsEmpty || nextLine.BoundingBox.IsEmpty) - return false; - - return IsWrappedParagraph( - currentLine.BoundingBox.Y, - currentLine.BoundingBox.Height, - nextLine.BoundingBox.Y, - nextLine.BoundingBox.Height); - } - - /// - /// Core paragraph-wrap heuristic: returns true when the vertical gap between two - /// lines is small enough (less than 60 % of the average line height) that they - /// belong to the same wrapped paragraph, and their heights are similar (ratio ≤ 1.5). - /// Works for any coordinate space — ratios are scale-invariant. - /// - internal static bool IsWrappedParagraph( - double currentTop, double currentHeight, - double nextTop, double nextHeight) - { - if (currentHeight <= 0 || nextHeight <= 0) - return false; - - // Lines with significantly different heights are likely different content blocks - double minHeight = Math.Min(currentHeight, nextHeight); - double maxHeight = Math.Max(currentHeight, nextHeight); - if (maxHeight / minHeight > 1.5) - return false; - - // Consecutive OCR entries must advance to a distinct visual row. Without - // this guard, duplicate or horizontally split entries on the same row have - // a negative gap and are incorrectly merged into a one-line-tall paragraph. - if (nextTop - currentTop < minHeight * 0.5) - return false; - - // If the vertical gap between line bounding boxes is less than 0.6× the average line - // height, the lines are part of the same paragraph (normal line spacing); otherwise - // the extra whitespace signals a paragraph break. - double gap = nextTop - (currentTop + currentHeight); - double avgLineHeight = (currentHeight + nextHeight) / 2.0; - return gap < avgLineHeight * 0.6; - } - - public static string GetStringFromOcrOutputs(List outputs) - { - StringBuilder text = new(); - - foreach (OcrOutput output in outputs) - { - output.CleanOutput(); - - if (!string.IsNullOrWhiteSpace(output.CleanedOutput)) - text.Append(output.CleanedOutput); - else if (!string.IsNullOrWhiteSpace(output.RawOutput)) - text.Append(output.RawOutput); - } - - return text.ToString(); - } - public static async Task OcrAbsoluteFilePathAsync(string absolutePath, ILanguage? language = null) { language ??= LanguageUtilities.GetCurrentInputLanguage(); @@ -916,7 +485,7 @@ public static async Task OcrAbsoluteFilePathAsync(string absolutePath, I } using Bitmap bmp = LoadBitmapFromFile(absolutePath); - return GetStringFromOcrOutputs(await GetTextFromImageAsync(bmp, language)); + return OcrUtilities.GetStringFromOcrOutputs(await GetTextFromImageAsync(bmp, language)); } private static Bitmap LoadBitmapFromFile(string absolutePath) @@ -953,7 +522,7 @@ public static async Task GetClickedWordAsync(Window passedWindow, Point private static async Task GetTextFromClickedWordAsync(Point singlePoint, Bitmap bitmap, ILanguage language) { - return GetTextFromClickedWord(singlePoint, await OcrUtilities.GetOcrResultFromImageAsync(bitmap, language)); + return GetTextFromClickedWord(singlePoint, await OcrSourceUtilities.GetOcrResultFromImageAsync(bitmap, language)); } private static string GetTextFromClickedWord(Point singlePoint, IOcrLinesWords ocrResult) @@ -974,7 +543,7 @@ public static async Task GetIdealScaleFactorForOcrAsync(Bitmap bitmap, I return 1.0; selectedLanguage = GetCompatibleOcrLanguage(selectedLanguage); - IOcrLinesWords ocrResult = await OcrUtilities.GetOcrResultFromImageAsync(bitmap, selectedLanguage); + IOcrLinesWords ocrResult = await OcrSourceUtilities.GetOcrResultFromImageAsync(bitmap, selectedLanguage); return GetIdealScaleFactorForOcrResult(ocrResult, bitmap.Height, bitmap.Width); } @@ -1015,22 +584,6 @@ private static double GetIdealScaleFactorForOcrResult(IOcrLinesWords ocrResult, return scaleFactor; } - public static Rect GetBoundingRect(this OcrLine ocrLine) - { - double top = ocrLine.Words.Select(x => x.BoundingRect.Top).Min(); - double bottom = ocrLine.Words.Select(x => x.BoundingRect.Bottom).Max(); - double left = ocrLine.Words.Select(x => x.BoundingRect.Left).Min(); - double right = ocrLine.Words.Select(x => x.BoundingRect.Right).Max(); - - return new() - { - X = left, - Y = top, - Width = Math.Abs(right - left), - Height = Math.Abs(bottom - top) - }; - } - public static async Task OcrFile(string path, ILanguage? selectedLanguage, OcrDirectoryOptions options) { StringBuilder returnString = new(); @@ -1077,9 +630,6 @@ public static async Task OcrFile(string path, ILanguage? selectedLanguag return returnString.ToString(); } - [GeneratedRegex(@"(^[\p{L}-[\p{Lo}]]|\p{Nd}$)|.{2,}")] - private static partial Regex SpaceJoiningWordRegex(); - private static async Task CanReplayPreviousFullscreenSelection(HistoryInfo history) { if (history.SelectionStyle is FsgSelectionStyle.Region or FsgSelectionStyle.AdjustAfter) diff --git a/Text-Grab/Utilities/PdfDocumentRenderer.cs b/Text-Grab/Utilities/PdfDocumentRenderer.cs index 3a60d4fa..18473d15 100644 --- a/Text-Grab/Utilities/PdfDocumentRenderer.cs +++ b/Text-Grab/Utilities/PdfDocumentRenderer.cs @@ -417,7 +417,7 @@ private async Task> GetOcrLinesAsync( Func? sourceRectPredicate = null) { using Bitmap bitmap = ImageMethods.BitmapSourceToBitmap(renderedPage); - (IOcrLinesWords? ocrResult, double scale) = await OcrUtilities.GetOcrResultFromBitmapAsync(bitmap, language); + (IOcrLinesWords? ocrResult, double scale) = await OcrSourceUtilities.GetOcrResultFromBitmapAsync(bitmap, language); if (ocrResult is null || ocrResult.Lines.Length == 0) return []; diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index 6c97b042..090d524e 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -4404,7 +4404,7 @@ await Parallel.ForEachAsync(ocrFileResults, parallelOptions, async (ocrFile, ct) { ct.ThrowIfCancellationRequested(); - ocrFile.OcrResult = await OcrUtilities.OcrFile(ocrFile.FilePath, selectedLanguage, options); + ocrFile.OcrResult = await OcrSourceUtilities.OcrFile(ocrFile.FilePath, selectedLanguage, options); // to get the TextBox to update whenever OCR Finishes: if (!options.WriteTxtFiles) @@ -4867,7 +4867,7 @@ private async void PasteExecuted(object sender, ExecutedRoutedEventArgs? e = nul { RandomAccessStreamReference streamReference = await dataPackageView.GetBitmapAsync(); using IRandomAccessStream stream = await streamReference.OpenReadAsync(); - List outputs = await OcrUtilities.GetTextFromRandomAccessStream(stream, LanguageUtilities.GetOCRLanguage()); + List outputs = await OcrSourceUtilities.GetTextFromRandomAccessStream(stream, LanguageUtilities.GetOCRLanguage()); string text = OcrUtilities.GetStringFromOcrOutputs(outputs); System.Windows.Application.Current.Dispatcher.Invoke(new Action(() => { AddCopiedTextToTextBox(text); })); @@ -4891,7 +4891,7 @@ private async void PasteExecuted(object sender, ExecutedRoutedEventArgs? e = nul continue; using IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.Read); - List outputs = await OcrUtilities.GetTextFromRandomAccessStream(stream, LanguageUtilities.GetOCRLanguage()); + List outputs = await OcrSourceUtilities.GetTextFromRandomAccessStream(stream, LanguageUtilities.GetOCRLanguage()); string text = OcrUtilities.GetStringFromOcrOutputs(outputs); System.Windows.Application.Current.Dispatcher.Invoke(new Action(() => { AddCopiedTextToTextBox(text); })); @@ -4913,7 +4913,7 @@ private async void PreviousRegion_Click(object sender, RoutedEventArgs e) HistoryService hs = Singleton.Instance; if (hs.HasAnyFullscreenHistory()) - await OcrUtilities.GetTextFromPreviousFullscreenRegion(PassedTextControl); + await OcrSourceUtilities.GetTextFromPreviousFullscreenRegion(PassedTextControl); } private async void RateAndReview_Click(object sender, RoutedEventArgs e) diff --git a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs index 2e0af306..76f02ec1 100644 --- a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs +++ b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs @@ -1265,28 +1265,28 @@ private async Task CommitSelectionCoreAsync(FullscreenCaptureResult select Math.Round(clickedPointForOcr.X), Math.Round(clickedPointForOcr.Y)); - TextFromOCR = await OcrUtilities.GetClickedWordAsync(this, clickedPointForOcr, selectedOcrLang); + TextFromOCR = await OcrSourceUtilities.GetClickedWordAsync(this, clickedPointForOcr, selectedOcrLang); } else if (selectedOcrLang is UiAutomationLang) { - TextFromOCR = await OcrUtilities.GetTextFromAbsoluteRectAsync(selection.CaptureRegion, selectedOcrLang, excludedHandles); + TextFromOCR = await OcrSourceUtilities.GetTextFromAbsoluteRectAsync(selection.CaptureRegion, selectedOcrLang, excludedHandles); } else if (selection.CapturedImage is not null) { TextFromOCR = isTable - ? await OcrUtilities.GetTextFromBitmapSourceAsTableAsync(selection.CapturedImage, selectedOcrLang) - : await OcrUtilities.GetTextFromBitmapSourceAsync(selection.CapturedImage, selectedOcrLang); + ? await OcrSourceUtilities.GetTextFromBitmapSourceAsTableAsync(selection.CapturedImage, selectedOcrLang) + : await OcrSourceUtilities.GetTextFromBitmapSourceAsync(selection.CapturedImage, selectedOcrLang); } else if (isTable) { // TODO: Look into why this happens and find a better way to dispose the bitmap // DO NOT add a using statement to this selected bitmap, it crashes the app Bitmap selectionBitmap = ImageMethods.GetRegionOfScreenAsBitmap(selection.CaptureRegion.AsRectangle()); - TextFromOCR = await OcrUtilities.GetTextFromBitmapAsTableAsync(selectionBitmap, selectedOcrLang); + TextFromOCR = await OcrSourceUtilities.GetTextFromBitmapAsTableAsync(selectionBitmap, selectedOcrLang); } else { - TextFromOCR = await OcrUtilities.GetTextFromAbsoluteRectAsync(selection.CaptureRegion, selectedOcrLang, excludedHandles); + TextFromOCR = await OcrSourceUtilities.GetTextFromAbsoluteRectAsync(selection.CaptureRegion, selectedOcrLang, excludedHandles); } } catch (Exception ex) diff --git a/Text-Grab/Views/GrabFrame.xaml.cs b/Text-Grab/Views/GrabFrame.xaml.cs index c7cd4caf..bc0a8fc9 100644 --- a/Text-Grab/Views/GrabFrame.xaml.cs +++ b/Text-Grab/Views/GrabFrame.xaml.cs @@ -1714,7 +1714,7 @@ private async void AddNewWordBorder(Border selectBorder) rect = new(rect.X + 4, rect.Y, (rect.Width * dpi.DpiScaleX) + 10, rect.Height * dpi.DpiScaleY); // Language language = CurrentLanguage.AsLanguage() ?? LanguageUtilities.GetCurrentInputLanguage().AsLanguage() ?? new Language("en-US"); ILanguage language = CurrentLanguage ?? LanguageUtilities.GetCurrentInputLanguage(); - string ocrText = await OcrUtilities.GetTextFromAbsoluteRectAsync( + string ocrText = await OcrSourceUtilities.GetTextFromAbsoluteRectAsync( rect.GetScaleSizeByFraction(viewBoxZoomFactor), language, GetUiAutomationExcludedHandles()); @@ -2345,11 +2345,11 @@ private async Task DrawOcrRectanglesAsync(string searchWord = "") if (frameContentImageSource is BitmapSource frozenBmp) { using System.Drawing.Bitmap bmpForOcr = ImageMethods.BitmapSourceToBitmap(frozenBmp); - (ocrResultOfWindow, windowFrameImageScale) = await OcrUtilities.GetOcrResultFromBitmapAsync(bmpForOcr, CurrentLanguage); + (ocrResultOfWindow, windowFrameImageScale) = await OcrSourceUtilities.GetOcrResultFromBitmapAsync(bmpForOcr, CurrentLanguage); } else { - (ocrResultOfWindow, windowFrameImageScale) = await OcrUtilities.GetOcrResultFromRegionAsync(rectCanvasSize, CurrentLanguage); + (ocrResultOfWindow, windowFrameImageScale) = await OcrSourceUtilities.GetOcrResultFromRegionAsync(rectCanvasSize, CurrentLanguage); } } diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index f27c1121..e65de29b 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -372,22 +372,34 @@ move only the strictly portable subset — furigana filtering, paragraph-wrap he `BuildTextFromOcrLines`, `GetStringFromOcrOutputs`, `GetTextFromOcrLine` — and leave engine dispatch behind. -**Blocker created by batch 3d — resolve before 4c.** `BuildTextFromOcrLines` is named above as -part of the "strictly portable subset", but `OcrUtilities.cs:642` calls `language.IsRightToLeft()`, -and 3d left both `IsRightToLeft` overloads in the app as `LanguageRtlExtensions` because -`XmlLanguage` comes from PresentationCore. `ReverseWordsForRightToLeft` on the next line is fine — -it is already in Core (`Extensions/StringBuilderExtensions.cs`). - -Only the `Language` overload actually needs `XmlLanguage`; the `ILanguage` overload needs it purely -because its `GlobalLang` branch delegates to the other one. Three options, in order of preference: -1. Give the `ILanguage` overload's `GlobalLang` branch a portable RTL check and move that overload - to Core.Windows, leaving the `Language`/`XmlLanguage` overload in the app. `XmlLanguage - .GetLanguage(tag).GetEquivalentCulture()` is close to `CultureInfo.GetCultureInfo(tag)` but not - identical — it walks up subtags and falls back to the invariant culture — so this is a real - behavior question, not a mechanical swap. Verify against RTL tags with subtags (`ar-EG`, - `he-IL`, `ur-PK`) before choosing it. -2. Route RTL through a settable hook the app registers at startup, same shape as `SettingsAccess`. -3. Leave `BuildTextFromOcrLines` in the app and shrink 4c's portable subset accordingly. +**Blocker created by batch 3d - RESOLVED in 4c.** `BuildTextFromOcrLines` calls +`language.IsRightToLeft()`, and 3d had left both `IsRightToLeft` overloads in the app as +`LanguageRtlExtensions` because `XmlLanguage` comes from PresentationCore. + +Option 1 was taken, after settling the behaviour question empirically rather than by reasoning: a +throwaway WPF probe compared `XmlLanguage.GetLanguage(tag).GetEquivalentCulture().TextInfo +.IsRightToLeft` against `CultureInfo.GetCultureInfo(tag).TextInfo.IsRightToLeft` across 24 tags - +`ar`, `ar-EG`, `ar-SA`, `he`, `he-IL`, `ur`, `ur-PK`, `fa`, `fa-IR`, `ckb`, `ps-AF`, `sd-Arab-PK`, +`yi`, `he-Hebr-IL`, `ar-XX`, `en`, `en-US`, `ja`, `zh-Hans`, `de-DE`, and the unresolvable `xx`, +`xx-YY`, `und` and `""`. They agreed on every one, including the tags with subtags and the ones +neither can resolve. + +So the `ILanguage` overload moved into Core.Windows `LanguageExtensions` with a `CultureInfo` +lookup in its `GlobalLang` branch, guarded by a `CultureNotFoundException` catch returning false +(XmlLanguage fell back to the invariant culture, which is LTR). That left the `Language` overload +with **zero** call sites - all five live `IsRightToLeft` calls are on `ILanguage` - so +`Extensions/LanguageRtlExtensions.cs` was deleted outright. 3d's facade is gone. + +**4c as executed.** The split went the direction the call-site census pointed: the portable text +assembly - `GetTextFromOcrLine`, `FilterFurigana`, `FilterFuriganaLines`, `OrderLinesForReadingFlow`, +`BuildTextFromOcrLines`, `ShouldUseParagraphDetection`, `GroupWrappedParagraphLines`, `IsWrappedLine`, +`IsWrappedParagraph`, `GetStringFromOcrOutputs`, `ParseOcrResultIntoWordBorderInfos`, and the nested +`PositionedOcrLine`/`GroupedOcrLines` - moved to Core.Windows **keeping the `OcrUtilities` name**, +because `Tests/OcrTests.cs` alone held 43 of the file's ~80 references and every one of them is +against that subset. The app-coupled half - capture, engine dispatch, file and `BitmapSource` +sources - took the new name `OcrSourceUtilities`. `GetBoundingRect(this OcrLine)` was deleted as +section 8 dead code. Engine dispatch stayed behind as the ordering note above predicted: +`WindowsAiUtilities` and `LanguageUtilities` have not moved. `Tests/OcrTests.cs` is the heaviest consumer of the headless surface and becomes a `Tests.Core.Windows` candidate in Wave 7. It references the nested `PositionedOcrLine` / @@ -405,6 +417,15 @@ Opus** — the workable split is to extract the pure `switch`-expression helpers `GetLanguageKind`, `GetPersistedLanguageIdentity`, `NormalizePersistedLanguageIdentity`) and leave the input-language reader in the app. +**Ordering constraint found while preparing 4c: 4e cannot precede 5a.** Reading `LanguageService` +in full, `InputLanguageManager` appears in exactly one place - the private +`GetCurrentInputLanguageTag()` - and everything else in the class is WinRT, which is legal in +Core.Windows. That makes the better split the whole class moving under its own name with the +input-language read behind a resolver the app registers (the `SettingsAccess` shape), defaulting +to `CultureInfo.CurrentUICulture.Name` when none is registered. But `GetAllLanguages()` and +`GetOCRLanguage()` both call `WindowsAiUtilities.CanDeviceUseWinAI()`, and `WindowsAiUtilities` is +still app-side, deferred on `SoftwareBitmapExtensions` - which is 5a. Run 4e after wave 5. + ### 4.5 Wave 5 — capture and imaging → Core.Windows **5a — move-now (after Wave 1):** @@ -665,7 +686,7 @@ Every row below was verified by reading the file, not inferred. | File | Blocker (specific) | Unblocked by | |---|---|---| -| `Utilities/OcrUtilities.cs` | `LoadBitmapFromFile` (887–899) builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite | 4c, and defer that one method | +| `Utilities/OcrSourceUtilities.cs` | Post-4c remainder. `LoadBitmapFromFile` builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite, and it takes `OcrAbsoluteFilePathAsync` and `OcrFile` with it. Engine dispatch additionally needs `WindowsAiUtilities` (5a) and `LanguageUtilities` (4e); the rest is `Window`/`BitmapSource` capture and stays | a GDI+/WIC rewrite, then 5a + 4e | | `Utilities/TesseractHelper.cs` | `TempImagePath()` calls `AutomationProfile.GetTemporaryDirectory()`. The settings write-back is **not** a blocker — `ITextGrabSettings.Save()` already covers it | 6a | | `Services/LanguageService.cs` | `System.Windows.Input.InputLanguageManager` (line 376), no portable substitute; reachable in production, not vestigial | Opus split (4e) | | `Utilities/WindowsAiUtilities.cs` | `AutomationProfile.GetTemporaryFilePath()` (109, 189); `SoftwareBitmapExtensions` unmoved; reads `Settings.Default.OverrideAiArchCheck` directly (61) | 6a + 5a + one interface add | @@ -675,7 +696,7 @@ Every row below was verified by reading the file, not inferred. | `Utilities/GrabFrameFileUtilities.cs` | Public signature bound to `HistoryInfo`, which needs B2 (`Rect PositionRect`), 2a (five enums) and 4e. A façade was considered and rejected as larger than the file it wraps | 2d + 4e, or `HistoryInfo` moving | | `Services/SettingsService.cs` | Clones `ButtonInfo` and `ShortcutKeySet` field-by-field (both never-move); `Windows.Storage.ApplicationDataContainer` caps it at Core.Windows regardless | needs a `ButtonInfo` redesign — likely never | | `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so the remaining blocker is a plain split | a split | -| `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam; OCR half blocked on 4c. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so that part of the original blocker is resolved | 4c + a façade | +| `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam. `GrabTemplate`/`TemplateRegion` moved to Core in 2d and 4c has landed, so the remaining blocker is the settings façade plus its calls into `OcrSourceUtilities` | a façade + `OcrSourceUtilities` | | `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | | `Services/HistoryService.cs` | Headless JSON pipeline interleaved with WPF menu building and `GrabFrame`/`EditTextWindow` construction, sharing private state | Opus split (6e) | @@ -689,7 +710,7 @@ and independent of any wave; two of them unblock real moves. | `OSInterop.GetAsyncKeyState(System.Windows.Forms.Keys)` (line 125) | The **only** `System.Windows.Forms` reference in all 1292 lines. Deleting it moves the whole file. | | `Models/DragDataObject.BitmapSourceToBitmap` (line 77) | The file's only WPF touchpoint, and a duplicate of `ImageMethods.BitmapSourceToBitmap`. Deleting it moves the file. | | `Models/ResultTable`: `OcrResult` property, `ParseOcrResultWordsIntoRects()`, the `ResultTable(ref List, DpiScale)` ctor, `CalculateResultRows`, `MergeTheseRowIDs` | Leftovers from a superseded grid-line algorithm. The `OcrResult` property is why `ResultTable` looked WinRT-coupled; the live path already uses `IOcrLinesWords`. | -| `Utilities/OcrUtilities.GetBoundingRect(this OcrLine)` (line 983) | `edefeaa` left it saying "other app code may still use it". Nothing does. | +| ~~`Utilities/OcrUtilities.GetBoundingRect(this OcrLine)`~~ | Deleted in 4c. `edefeaa` had left it saying "other app code may still use it"; nothing did. | | `Extensions/ImageExtensions.ExifRotate` (line 12) | Unused. | Not dead, but a one-line dependency removal in the same spirit: From 9ef31719efaed24ffff8c9b416114b6b8c5e5ebb Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:17:00 -0500 Subject: [PATCH 31/54] Move HDR tone-mapping and imaging leaves into Core/Core.Windows Batch 5a of the Core split. Six move-now files, no splits needed: - Utilities/Hdr/HdrToneMapper.cs -> Text-Grab.Core (nothing but System). - Utilities/Hdr/DisplayHdrInfo.cs -> Text-Grab.Core.Windows. Added Vortice.Direct3D11 and Vortice.DXGI (3.8.3, matching the app's pinned versions) for the IDXGIOutput6 enumeration. - Extensions/ImageExtensions.cs -> Text-Grab.Core.Windows. Deleted the dead ExifRotate first (section 8): re-verified zero call sites by grep - the only reference to the name was its own internal call to GetRotateFlipType(this Image), which is unrelated and still live via ImageMethods.GetRotateFlipType(string). - Utilities/ImageChangeDetector.cs -> Text-Grab.Core.Windows. Added Magick.NET-Q16-AnyCPU and Magick.NET.SystemDrawing (14.16.0 / 8.0.25, matching the app). - Models/DragDataObject.cs -> Text-Grab.Core.Windows. Deleted the dead BitmapSourceToBitmap first (section 8): re-verified zero call sites - every BitmapSourceToBitmap call in the repo resolves to the live, identically-named ImageMethods.BitmapSourceToBitmap, a different method on a different type that a bare grep would conflate with this one. - Extensions/SoftwareBitmapExtensions.cs -> Text-Grab.Core.Windows. Needed StorageFileExtensions and WrappingStream (both already in Core.Windows/ Core from Wave 1), plus one dependency the plan didn't list: Microsoft.UI.Xaml.Media.Imaging.SoftwareBitmapSource (ToSourceAsync) comes from the WinUI3 projection, which the app pulls in via Microsoft.WindowsAppSDK.WinUI. Added that package (2.3.6, matching the app) to Core.Windows - the compiler flagged it as CS0234/CS0246, and it's a WinAppSDK/WinRT projection, not WPF, so it doesn't touch the UseWPF=false boundary TierBoundaryTests enforces. Self-assembly-reflection audit: grepped all six files for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and AppContext.BaseDirectory - zero matches. No files deferred from this batch's list; all six moved cleanly. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 assembly-conflict warnings only). Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 --- .../Extensions/ImageExtensions.cs | 10 ---------- .../Extensions/SoftwareBitmapExtensions.cs | 0 .../Models/DragDataObject.cs | 19 ------------------- .../Text-Grab.Core.Windows.csproj | 8 ++++++++ .../Utilities/Hdr/DisplayHdrInfo.cs | 0 .../Utilities/ImageChangeDetector.cs | 0 .../Utilities/Hdr/HdrToneMapper.cs | 0 7 files changed, 8 insertions(+), 29 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Extensions/ImageExtensions.cs (78%) rename {Text-Grab => Text-Grab.Core.Windows}/Extensions/SoftwareBitmapExtensions.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Models/DragDataObject.cs (78%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/Hdr/DisplayHdrInfo.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/ImageChangeDetector.cs (100%) rename {Text-Grab => Text-Grab.Core}/Utilities/Hdr/HdrToneMapper.cs (100%) diff --git a/Text-Grab/Extensions/ImageExtensions.cs b/Text-Grab.Core.Windows/Extensions/ImageExtensions.cs similarity index 78% rename from Text-Grab/Extensions/ImageExtensions.cs rename to Text-Grab.Core.Windows/Extensions/ImageExtensions.cs index be8c0720..0aa44755 100644 --- a/Text-Grab/Extensions/ImageExtensions.cs +++ b/Text-Grab.Core.Windows/Extensions/ImageExtensions.cs @@ -9,16 +9,6 @@ internal static class ImageExtensions { private const int exifOrientationID = 0x112; //274 - internal static void ExifRotate(this Image img) - { - RotateFlipType rot = img.GetRotateFlipType(); - if (rot != RotateFlipType.RotateNoneFlipNone) - { - img.RotateFlip(rot); - img.RemovePropertyItem(exifOrientationID); - } - } - internal static RotateFlipType GetRotateFlipType(this Image img) { if (!img.PropertyIdList.Contains(exifOrientationID) diff --git a/Text-Grab/Extensions/SoftwareBitmapExtensions.cs b/Text-Grab.Core.Windows/Extensions/SoftwareBitmapExtensions.cs similarity index 100% rename from Text-Grab/Extensions/SoftwareBitmapExtensions.cs rename to Text-Grab.Core.Windows/Extensions/SoftwareBitmapExtensions.cs diff --git a/Text-Grab/Models/DragDataObject.cs b/Text-Grab.Core.Windows/Models/DragDataObject.cs similarity index 78% rename from Text-Grab/Models/DragDataObject.cs rename to Text-Grab.Core.Windows/Models/DragDataObject.cs index 55d861df..113cbeab 100644 --- a/Text-Grab/Models/DragDataObject.cs +++ b/Text-Grab.Core.Windows/Models/DragDataObject.cs @@ -2,8 +2,6 @@ using System.Drawing; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; -using DrawingImaging = System.Drawing.Imaging; -using MediaImaging = System.Windows.Media.Imaging; namespace Text_Grab.Models; @@ -72,21 +70,4 @@ private interface IDragSourceHelper // more methods available, but we don't need them } - - // https://stackoverflow.com/a/2897325 - public static Bitmap? BitmapSourceToBitmap(MediaImaging.BitmapSource source) - { - if (source == null) - { - return null; - } - - Bitmap bitmap = new(source.PixelWidth, source.PixelHeight, DrawingImaging.PixelFormat.Format32bppArgb); - DrawingImaging.BitmapData bitmapData = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), DrawingImaging.ImageLockMode.WriteOnly, DrawingImaging.PixelFormat.Format32bppArgb); - - source.CopyPixels(System.Windows.Int32Rect.Empty, bitmapData.Scan0, bitmapData.Height * bitmapData.Stride, bitmapData.Stride); - bitmap.UnlockBits(bitmapData); - - return bitmap; - } } \ No newline at end of file diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index 4e33bb6e..f5f64348 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -46,10 +46,18 @@ + + + + + + + + diff --git a/Text-Grab/Utilities/Hdr/DisplayHdrInfo.cs b/Text-Grab.Core.Windows/Utilities/Hdr/DisplayHdrInfo.cs similarity index 100% rename from Text-Grab/Utilities/Hdr/DisplayHdrInfo.cs rename to Text-Grab.Core.Windows/Utilities/Hdr/DisplayHdrInfo.cs diff --git a/Text-Grab/Utilities/ImageChangeDetector.cs b/Text-Grab.Core.Windows/Utilities/ImageChangeDetector.cs similarity index 100% rename from Text-Grab/Utilities/ImageChangeDetector.cs rename to Text-Grab.Core.Windows/Utilities/ImageChangeDetector.cs diff --git a/Text-Grab/Utilities/Hdr/HdrToneMapper.cs b/Text-Grab.Core/Utilities/Hdr/HdrToneMapper.cs similarity index 100% rename from Text-Grab/Utilities/Hdr/HdrToneMapper.cs rename to Text-Grab.Core/Utilities/Hdr/HdrToneMapper.cs From bbf601bb5b19ace2fbaae9c5360599ad71d93a47 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:21:25 -0500 Subject: [PATCH 32/54] Split ImageMethods.cs, move the headless bitmap helpers to Core.Windows Batch 5c of the Core split. Call-site census before deciding the split direction: PadImage (0 external callers - only used internally by GetRegionOfScreenAsBitmap, which stays), GetBitmapFromIRandomAccessStream (3: BarcodeUtilitiesTests, OcrSourceUtilities, PdfDocumentRenderer) and GetRotateFlipType(string) (2: GrabFrame.xaml.cs, OcrSourceUtilities) total 5 external call sites. Everything else in the file - BitmapImageToBitmap, BitmapToImageSource, CachedBitmapToBitmapImage, GetRegionOfScreenAsBitmap, GetWindowsBoundsBitmap, GetWindowBoundsImage, ScaleBitmapUniform, InteropBitmapToBitmap, BitmapSourceToBitmap, ImageSourceToBitmap, GetBitmapImageFromIRandomAccessStream, RotateImage - has on the order of 60 call sites across GrabFrame.xaml.cs, FullscreenGrab, MagickHelpers, NotifyIconUtilities, OcrSourceUtilities and PdfDocumentRenderer. Unlike 4c, the census here points the other way: the portable half is the minority, so it took the new name (Text-Grab.Core.Windows/Utilities/ BitmapUtilities.cs) and the app-bound majority kept ImageMethods. CaptureScreenRegion did not move, and is recorded as a new row in section 7. It calls HdrScreenCapture.TryCaptureRegion when HdrCaptureCorrection is set, and HdrScreenCapture.cs is explicitly out of this batch's scope (batch 5b, being done separately, needs a settable dispatcher hook first). Core.Windows cannot reference the app, so CaptureScreenRegion cannot move until HdrScreenCapture does. Its only two callers (GetRegionOfScreenAsBitmap, GetWindowsBoundsBitmap) already stay behind for their own reasons (invariant 5's HistoryService caching, and the GrabFrame View pattern-match), so nothing else was blocked by leaving it. Because CaptureScreenRegion stayed, its AppUtilities.TextGrabSettings.HdrCaptureCorrection read stayed with it - HdrCaptureCorrection was deliberately NOT added to ITextGrabSettings, since nothing that actually moved reads it. BitmapUtilities.cs keeps ImageMethods' original namespace (Text_Grab, not Text_Grab.Utilities - the file was always oddly namespaced) per invariant 1. GetRotateFlipType(string) still calls the GetRotateFlipType(this Image) extension from Extensions/ImageExtensions.cs, already in Core.Windows since 5a. Self-assembly-reflection audit: grepped BitmapUtilities.cs for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and AppContext.BaseDirectory - zero matches. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 assembly-conflict warnings only). Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 --- Tests/BarcodeUtilitiesTests.cs | 2 +- .../Utilities/BitmapUtilities.cs | 45 +++++++++++++++++++ Text-Grab/Utilities/ImageMethods.cs | 37 +-------------- Text-Grab/Utilities/OcrSourceUtilities.cs | 4 +- Text-Grab/Utilities/PdfDocumentRenderer.cs | 2 +- Text-Grab/Views/GrabFrame.xaml.cs | 2 +- docs/Core-Split-Plan.md | 1 + 7 files changed, 52 insertions(+), 41 deletions(-) create mode 100644 Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs diff --git a/Tests/BarcodeUtilitiesTests.cs b/Tests/BarcodeUtilitiesTests.cs index d4856aff..dcdda410 100644 --- a/Tests/BarcodeUtilitiesTests.cs +++ b/Tests/BarcodeUtilitiesTests.cs @@ -73,7 +73,7 @@ public async Task GetBitmapFromIRandomAccessStream_ReturnsBitmapIndependentOfSou using InMemoryRandomAccessStream randomAccessStream = new(); _ = await randomAccessStream.WriteAsync(memoryStream.ToArray().AsBuffer()); - Bitmap clonedBitmap = ImageMethods.GetBitmapFromIRandomAccessStream(randomAccessStream); + Bitmap clonedBitmap = BitmapUtilities.GetBitmapFromIRandomAccessStream(randomAccessStream); Assert.Equal(8, clonedBitmap.Width); Assert.Equal(8, clonedBitmap.Height); diff --git a/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs b/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs new file mode 100644 index 00000000..e27b4ba1 --- /dev/null +++ b/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs @@ -0,0 +1,45 @@ +using System; +using System.Drawing; +using System.IO; +using Text_Grab.Extensions; +using Windows.Storage.Streams; + +namespace Text_Grab; + +public static class BitmapUtilities +{ + public static Bitmap PadImage(Bitmap image, int minW = 64, int minH = 64) + { + if (image.Height >= minH && image.Width >= minW) + return image; + + int width = Math.Max(image.Width + 16, minW + 16); + int height = Math.Max(image.Height + 16, minH + 16); + + // Create a compatible bitmap + Bitmap destination = new(width, height, image.PixelFormat); + using Graphics gd = Graphics.FromImage(destination); + + gd.Clear(image.GetPixel(0, 0)); + gd.DrawImageUnscaled(image, 8, 8); + + return destination; + } + + public static Bitmap GetBitmapFromIRandomAccessStream(IRandomAccessStream stream) + { + Stream managedStream = stream.AsStream(); + if (managedStream.CanSeek) + managedStream.Position = 0; + + using Bitmap bitmap = new(managedStream); + return new Bitmap(bitmap); + } + + internal static RotateFlipType GetRotateFlipType(string path) + { + using Image img = Image.FromFile(path); + RotateFlipType rotateFlipType = img.GetRotateFlipType(); + return rotateFlipType; + } +} diff --git a/Text-Grab/Utilities/ImageMethods.cs b/Text-Grab/Utilities/ImageMethods.cs index 5c22e241..0e0866e6 100644 --- a/Text-Grab/Utilities/ImageMethods.cs +++ b/Text-Grab/Utilities/ImageMethods.cs @@ -19,24 +19,6 @@ namespace Text_Grab; public static class ImageMethods { - public static Bitmap PadImage(Bitmap image, int minW = 64, int minH = 64) - { - if (image.Height >= minH && image.Width >= minW) - return image; - - int width = Math.Max(image.Width + 16, minW + 16); - int height = Math.Max(image.Height + 16, minH + 16); - - // Create a compatible bitmap - Bitmap destination = new(width, height, image.PixelFormat); - using Graphics gd = Graphics.FromImage(destination); - - gd.Clear(image.GetPixel(0, 0)); - gd.DrawImageUnscaled(image, 8, 8); - - return destination; - } - public static Bitmap BitmapImageToBitmap(BitmapImage bitmapImage) { using MemoryStream outStream = new(); @@ -116,7 +98,7 @@ private static Bitmap CaptureScreenRegion(Rectangle region) public static Bitmap GetRegionOfScreenAsBitmap(Rectangle region, bool cacheResult = true) { Bitmap bmp = CaptureScreenRegion(region); - bmp = PadImage(bmp); + bmp = BitmapUtilities.PadImage(bmp); if (cacheResult) Singleton.Instance.CacheLastBitmap(bmp); @@ -246,16 +228,6 @@ public static Bitmap BitmapSourceToBitmap(BitmapSource source) }; } - public static Bitmap GetBitmapFromIRandomAccessStream(IRandomAccessStream stream) - { - Stream managedStream = stream.AsStream(); - if (managedStream.CanSeek) - managedStream.Position = 0; - - using Bitmap bitmap = new(managedStream); - return new Bitmap(bitmap); - } - public static BitmapImage GetBitmapImageFromIRandomAccessStream(IRandomAccessStream stream) { BitmapImage bmp = new(); @@ -270,13 +242,6 @@ public static BitmapImage GetBitmapImageFromIRandomAccessStream(IRandomAccessStr return bmp; } - internal static RotateFlipType GetRotateFlipType(string path) - { - using Image img = Image.FromFile(path); - RotateFlipType rotateFlipType = img.GetRotateFlipType(); - return rotateFlipType; - } - internal static void RotateImage(BitmapImage droppedImage, RotateFlipType rotateFlipType) { // Only consider basic rotation for now diff --git a/Text-Grab/Utilities/OcrSourceUtilities.cs b/Text-Grab/Utilities/OcrSourceUtilities.cs index 7acbdcde..d9ed53e0 100644 --- a/Text-Grab/Utilities/OcrSourceUtilities.cs +++ b/Text-Grab/Utilities/OcrSourceUtilities.cs @@ -373,7 +373,7 @@ public static async Task GetTextFromPreviousFullscreenRegion(TextBox? destinatio public static async Task> GetTextFromRandomAccessStream(IRandomAccessStream randomAccessStream, ILanguage language) { - Bitmap bitmap = ImageMethods.GetBitmapFromIRandomAccessStream(randomAccessStream); + Bitmap bitmap = BitmapUtilities.GetBitmapFromIRandomAccessStream(randomAccessStream); List outputs = await GetTextFromImageAsync(bitmap, language); return outputs; } @@ -491,7 +491,7 @@ public static async Task OcrAbsoluteFilePathAsync(string absolutePath, I private static Bitmap LoadBitmapFromFile(string absolutePath) { Uri fileURI = new(absolutePath, UriKind.Absolute); - RotateFlipType rotateFlipType = ImageMethods.GetRotateFlipType(absolutePath); + RotateFlipType rotateFlipType = BitmapUtilities.GetRotateFlipType(absolutePath); BitmapImage droppedImage = new(); droppedImage.BeginInit(); droppedImage.UriSource = fileURI; diff --git a/Text-Grab/Utilities/PdfDocumentRenderer.cs b/Text-Grab/Utilities/PdfDocumentRenderer.cs index 18473d15..d067963e 100644 --- a/Text-Grab/Utilities/PdfDocumentRenderer.cs +++ b/Text-Grab/Utilities/PdfDocumentRenderer.cs @@ -472,7 +472,7 @@ private static async Task RenderPageBitmapAsync(WinPdfPage page) await page.RenderToStreamAsync(renderedStream, renderOptions); renderedStream.Seek(0); - using Bitmap renderedBitmap = ImageMethods.GetBitmapFromIRandomAccessStream(renderedStream); + using Bitmap renderedBitmap = BitmapUtilities.GetBitmapFromIRandomAccessStream(renderedStream); return ImageMethods.BitmapToImageSource(renderedBitmap); } diff --git a/Text-Grab/Views/GrabFrame.xaml.cs b/Text-Grab/Views/GrabFrame.xaml.cs index bc0a8fc9..9b705f49 100644 --- a/Text-Grab/Views/GrabFrame.xaml.cs +++ b/Text-Grab/Views/GrabFrame.xaml.cs @@ -4870,7 +4870,7 @@ private async Task TryLoadImageFromPath(string path) droppedImage.BeginInit(); droppedImage.UriSource = fileURI; droppedImage.CacheOption = BitmapCacheOption.OnLoad; // decode fully into memory and release the file handle - System.Drawing.RotateFlipType rotateFlipType = ImageMethods.GetRotateFlipType(path); + System.Drawing.RotateFlipType rotateFlipType = BitmapUtilities.GetRotateFlipType(path); ImageMethods.RotateImage(droppedImage, rotateFlipType); droppedImage.EndInit(); frameContentImageSource = droppedImage; diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index e65de29b..f9845229 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -699,6 +699,7 @@ Every row below was verified by reading the file, not inferred. | `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam. `GrabTemplate`/`TemplateRegion` moved to Core in 2d and 4c has landed, so the remaining blocker is the settings façade plus its calls into `OcrSourceUtilities` | a façade + `OcrSourceUtilities` | | `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | | `Services/HistoryService.cs` | Headless JSON pipeline interleaved with WPF menu building and `GrabFrame`/`EditTextWindow` construction, sharing private state | Opus split (6e) | +| `Utilities/ImageMethods.cs: CaptureScreenRegion` | Calls `HdrScreenCapture.TryCaptureRegion`, which stays in the app pending 5b's settable dispatcher hook. Core.Windows cannot reference the app, so this cannot move until `HdrScreenCapture.cs` does. `PadImage`, `GetBitmapFromIRandomAccessStream` and `GetRotateFlipType(string)` moved on their own to `Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs`; `HdrCaptureCorrection` was **not** added to `ITextGrabSettings` because nothing that actually moved reads it - `CaptureScreenRegion` still reads it via `AppUtilities.TextGrabSettings` in the app | 5b | ## 8. Verified dead code — free, zero-risk prep From 60c7b21877a9fe781032203a4e46166ba88c3039 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:24:42 -0500 Subject: [PATCH 33/54] Split ClipboardUtilities.cs, move the CF_HTML table parser to plain Core Batch 5d of the Core split. ~330 of the file's 464 lines were a pure CF_HTML table parser with no clipboard, WPF, WinRT or GDI+ dependency: BuildCfHtmlTable, WrapHtmlFragmentAsCfHtml, ConvertHtmlToTabSeparated, ExtractHtmlFragment, ParseHtmlTableToGrid, FindNextFreeColumnRange, ParseHtmlRowCells, ParseSpanAttribute, CleanHtmlCellContent, and the MaxHtmlTableSpan constant. Moved as-is to Text-Grab.Core/Utilities/CfHtmlTableUtilities.cs, keeping the Text_Grab.Utilities namespace. WrapHtmlFragmentAsCfHtml and ConvertHtmlToTabSeparated stayed internal - Text-Grab.Core already grants InternalsVisibleTo to Text-Grab and Tests, so both the app's remaining TryGetHtmlTableAsTabSeparated (now calling CfHtmlTableUtilities.ConvertHtmlToTabSeparated) and the existing tests kept working with no visibility change. The clipboard-touching methods stayed in Text-Grab/Utilities/ClipboardUtilities.cs: TryGetClipboardText, TryGetImageFromClipboard, GetBase64ClipboardContentAsImageSource, ClipboardContainsBase64Image, CleanTeamsBase64Image, TryGetHtmlTableAsTabSeparated, base64ImageExtension. Separately, as instructed regardless of the split: line 64's System.Windows.Forms.DataFormats.Bitmap (the file's only WinForms use) was swapped for System.Windows.DataFormats.Bitmap - the identical string constant, now sourced from WPF instead of WinForms. Call sites updated: EditTextWindow.xaml.cs (BuildCfHtmlTable), Tests/ClipboardUtilitiesTests.cs (BuildCfHtmlTable and ConvertHtmlToTabSeparated, throughout), Tests/EditTextWindowSpreadsheetTests.cs (the fully-qualified ConvertHtmlToTabSeparated call). None of these test files moved to Tests.Core in this batch - that migration is Wave 7a's job, not this one's. Self-assembly-reflection audit: grepped both files for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and AppContext.BaseDirectory - zero matches. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 assembly-conflict warnings only). Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 --- Tests/ClipboardUtilitiesTests.cs | 30 +- Tests/EditTextWindowSpreadsheetTests.cs | 2 +- .../Utilities/CfHtmlTableUtilities.cs | 302 ++++++++++++++++++ Text-Grab/Utilities/ClipboardUtilities.cs | 289 +---------------- Text-Grab/Views/EditTextWindow.xaml.cs | 2 +- 5 files changed, 321 insertions(+), 304 deletions(-) create mode 100644 Text-Grab.Core/Utilities/CfHtmlTableUtilities.cs diff --git a/Tests/ClipboardUtilitiesTests.cs b/Tests/ClipboardUtilitiesTests.cs index 22d2c092..38b93e15 100644 --- a/Tests/ClipboardUtilitiesTests.cs +++ b/Tests/ClipboardUtilitiesTests.cs @@ -36,7 +36,7 @@ public class ClipboardUtilitiesTests [Fact] public void ConvertHtmlToTabSeparated_ParsesBasicTable() { - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(SampleCfHtml); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(SampleCfHtml); string[] lines = result.Split('\n'); Assert.Equal(3, lines.Length); @@ -54,7 +54,7 @@ public void ConvertHtmlToTabSeparated_HandlesBrTag() """; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); Assert.Equal("4 A\tSpring", result); } @@ -63,7 +63,7 @@ public void ConvertHtmlToTabSeparated_HandlesBrTag() public void ConvertHtmlToTabSeparated_ReturnsEmptyWhenNoTable() { string html = "

No table here

"; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); Assert.Empty(result); } @@ -76,7 +76,7 @@ public void ConvertHtmlToTabSeparated_DecodesHtmlEntities() """; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); Assert.Equal("A & B\t", result); } @@ -91,7 +91,7 @@ public void ConvertHtmlToTabSeparated_HandlesThElements() """; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); string[] lines = result.Split('\n'); Assert.Equal(2, lines.Length); @@ -109,7 +109,7 @@ public void ConvertHtmlToTabSeparated_HandlesColspan() """; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); string[] lines = result.Split('\n'); Assert.Equal(2, lines.Length); @@ -127,7 +127,7 @@ public void ConvertHtmlToTabSeparated_HandlesRowspan() """; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); string[] lines = result.Split('\n'); Assert.Equal(2, lines.Length); @@ -145,7 +145,7 @@ public void ConvertHtmlToTabSeparated_DoesNotOverwriteRowspanWithColspan() """; - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); string[] lines = result.Split('\n'); Assert.Equal(2, lines.Length); @@ -173,7 +173,7 @@ public void ConvertHtmlToTabSeparated_DoesNotOverwriteRowspanWithColspan() [Fact] public void ConvertHtmlToTabSeparated_ParsesBrowserExtensionRegionTable() { - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(ExtensionRegionTableCfHtml); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(ExtensionRegionTableCfHtml); string[] lines = result.Split('\n'); Assert.Equal(3, lines.Length); @@ -186,13 +186,13 @@ public void ConvertHtmlToTabSeparated_ParsesBrowserExtensionRegionTable() [Fact] public void BuildCfHtmlTable_RoundTripsThroughConvertHtmlToTabSeparated() { - string cfHtml = ClipboardUtilities.BuildCfHtmlTable( + string cfHtml = CfHtmlTableUtilities.BuildCfHtmlTable( [ ["Month", "Int", "Season"], ["January", "1", "Winter"], ]); - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(cfHtml); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(cfHtml); string[] lines = result.Split('\n'); Assert.Equal(2, lines.Length); @@ -203,7 +203,7 @@ public void BuildCfHtmlTable_RoundTripsThroughConvertHtmlToTabSeparated() [Fact] public void BuildCfHtmlTable_HeaderOffsetsPointAtFragmentBoundaries() { - string cfHtml = ClipboardUtilities.BuildCfHtmlTable([["a", "b"]]); + string cfHtml = CfHtmlTableUtilities.BuildCfHtmlTable([["a", "b"]]); int startHtml = int.Parse(cfHtml.Substring(cfHtml.IndexOf("StartHTML:") + "StartHTML:".Length, 10)); int endHtml = int.Parse(cfHtml.Substring(cfHtml.IndexOf("EndHTML:") + "EndHTML:".Length, 10)); @@ -224,9 +224,9 @@ public void BuildCfHtmlTable_HeaderOffsetsPointAtFragmentBoundaries() [Fact] public void BuildCfHtmlTable_EscapesHtmlAndConvertsNewlinesToBreaks() { - string cfHtml = ClipboardUtilities.BuildCfHtmlTable([["A & B", "line1\r\nline2"]]); + string cfHtml = CfHtmlTableUtilities.BuildCfHtmlTable([["A & B", "line1\r\nline2"]]); - string result = ClipboardUtilities.ConvertHtmlToTabSeparated(cfHtml); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(cfHtml); Assert.Equal("A & B\tline1 line2", result); } @@ -234,6 +234,6 @@ public void BuildCfHtmlTable_EscapesHtmlAndConvertsNewlinesToBreaks() [Fact] public void BuildCfHtmlTable_ReturnsEmptyForNoRows() { - Assert.Equal(string.Empty, ClipboardUtilities.BuildCfHtmlTable([])); + Assert.Equal(string.Empty, CfHtmlTableUtilities.BuildCfHtmlTable([])); } } diff --git a/Tests/EditTextWindowSpreadsheetTests.cs b/Tests/EditTextWindowSpreadsheetTests.cs index 75d54c06..6cb80f7c 100644 --- a/Tests/EditTextWindowSpreadsheetTests.cs +++ b/Tests/EditTextWindowSpreadsheetTests.cs @@ -139,7 +139,7 @@ public void BuildSpreadsheetSelectionHtml_IncludesOnlySelectedCellsAsTableRows() (5, 5) ]); - string tabSeparated = Text_Grab.Utilities.ClipboardUtilities.ConvertHtmlToTabSeparated(html); + string tabSeparated = Text_Grab.Utilities.CfHtmlTableUtilities.ConvertHtmlToTabSeparated(html); Assert.Equal("a1\tc1" + Environment.NewLine + "a2\tc2", tabSeparated.Replace("\n", Environment.NewLine)); } diff --git a/Text-Grab.Core/Utilities/CfHtmlTableUtilities.cs b/Text-Grab.Core/Utilities/CfHtmlTableUtilities.cs new file mode 100644 index 00000000..2185dc72 --- /dev/null +++ b/Text-Grab.Core/Utilities/CfHtmlTableUtilities.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; + +namespace Text_Grab.Utilities; + +/// +/// Pure CF_HTML table parsing and serialization - no clipboard, WPF, WinRT or GDI+ dependency. +/// Builds the CF_HTML fragment Windows' clipboard format expects and parses one back into a +/// tab-separated grid. Split out of ClipboardUtilities, whose remaining clipboard-touching +/// methods call into this for the actual table encoding/decoding. +/// +public static class CfHtmlTableUtilities +{ + private const int MaxHtmlTableSpan = 16_384; + + public static string BuildCfHtmlTable(IReadOnlyList> rows) + { + if (rows is null || rows.Count == 0) + return string.Empty; + + StringBuilder table = new(); + table.Append(""); + + foreach (IReadOnlyList row in rows) + { + table.Append(""); + foreach (string cell in row) + { + table.Append(""); + } + table.Append(""); + } + + table.Append("
"); + table.Append(WebUtility.HtmlEncode(cell ?? string.Empty).Replace("\r\n", "
").Replace("\n", "
")); + table.Append("
"); + + return WrapHtmlFragmentAsCfHtml(table.ToString()); + } + + internal static string WrapHtmlFragmentAsCfHtml(string htmlFragment) + { + const string htmlPrefix = "\r\n\r\n"; + const string htmlSuffix = "\r\n\r\n\r\n"; + + // CF_HTML header fields are 10-digit, zero-padded byte offsets into the UTF-8 + // encoded clipboard payload. See https://learn.microsoft.com/windows/win32/dataxchg/html-clipboard-format + static string BuildHeader(int startHtml, int endHtml, int startFragment, int endFragment) => + "Version:0.9\r\n" + + $"StartHTML:{startHtml:D10}\r\n" + + $"EndHTML:{endHtml:D10}\r\n" + + $"StartFragment:{startFragment:D10}\r\n" + + $"EndFragment:{endFragment:D10}\r\n"; + + int headerByteLength = Encoding.UTF8.GetByteCount(BuildHeader(0, 0, 0, 0)); + int startHtmlOffset = headerByteLength; + int startFragmentOffset = startHtmlOffset + Encoding.UTF8.GetByteCount(htmlPrefix); + int endFragmentOffset = startFragmentOffset + Encoding.UTF8.GetByteCount(htmlFragment); + int endHtmlOffset = endFragmentOffset + Encoding.UTF8.GetByteCount(htmlSuffix); + + return BuildHeader(startHtmlOffset, endHtmlOffset, startFragmentOffset, endFragmentOffset) + + htmlPrefix + htmlFragment + htmlSuffix; + } + + internal static string ConvertHtmlToTabSeparated(string cfHtml) + { + string fragment = ExtractHtmlFragment(cfHtml); + List> table = ParseHtmlTableToGrid(fragment); + if (table.Count == 0) + return string.Empty; + + StringBuilder sb = new(); + for (int r = 0; r < table.Count; r++) + { + if (r > 0) sb.Append('\n'); + sb.Append(string.Join("\t", table[r])); + } + return sb.ToString(); + } + + private static string ExtractHtmlFragment(string cfHtml) + { + int startPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); + if (startPos < 0) + startPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); + + int endPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); + if (endPos < 0) + endPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); + + if (startPos >= 0 && endPos > startPos) + { + int fragmentStart = cfHtml.IndexOf("-->", startPos) + 3; + return cfHtml[fragmentStart..endPos]; + } + + // Fall back to byte-offset headers (StartFragment:/EndFragment:) + const string startKey = "StartFragment:"; + const string endKey = "EndFragment:"; + int sfIdx = cfHtml.IndexOf(startKey, StringComparison.OrdinalIgnoreCase); + int efIdx = cfHtml.IndexOf(endKey, StringComparison.OrdinalIgnoreCase); + + if (sfIdx >= 0 && efIdx >= 0) + { + int sfNumStart = sfIdx + startKey.Length; + int sfLineEnd = cfHtml.IndexOf('\n', sfNumStart); + int efNumStart = efIdx + endKey.Length; + int efLineEnd = cfHtml.IndexOf('\n', efNumStart); + + if (sfLineEnd > sfNumStart && efLineEnd > efNumStart + && int.TryParse(cfHtml[sfNumStart..sfLineEnd].Trim(), out int sfOff) + && int.TryParse(cfHtml[efNumStart..efLineEnd].Trim(), out int efOff) + && sfOff >= 0 && efOff > sfOff && efOff <= cfHtml.Length) + { + return cfHtml[sfOff..efOff]; + } + } + + return cfHtml; + } + + private static List> ParseHtmlTableToGrid(string html) + { + List> result = []; + int tableStart = html.IndexOf("", StringComparison.OrdinalIgnoreCase); + tableEnd = tableEnd >= 0 ? tableEnd + 8 : html.Length; + + string tableHtml = html[tableStart..tableEnd]; + + // Tracks cells that span into future rows: col -> (remaining rows to fill, cell content) + Dictionary rowspanMap = []; + + int pos = 0; + while (pos < tableHtml.Length) + { + int rowStart = tableHtml.IndexOf("", rowStart, StringComparison.OrdinalIgnoreCase); + rowEnd = rowEnd >= 0 ? rowEnd + 5 : tableHtml.Length; + + List<(string Text, int ColSpan, int RowSpan)> parsedCells = + ParseHtmlRowCells(tableHtml[rowStart..rowEnd]); + + if (parsedCells.Count > 0 || rowspanMap.Count > 0) + { + // Build a sparse column map for this row + Dictionary rowData = []; + + // Apply rowspan carry-overs from previous rows first + foreach (int col in rowspanMap.Keys.OrderBy(k => k).ToList()) + { + (int rem, string content) = rowspanMap[col]; + rowData[col] = content; + if (rem > 1) + rowspanMap[col] = (rem - 1, content); + else + rowspanMap.Remove(col); + } + + // Place each parsed cell in the next free column(s) + int nextFreeCol = 0; + foreach ((string text, int colspan, int rowspan) in parsedCells) + { + nextFreeCol = FindNextFreeColumnRange(rowData, nextFreeCol, colspan); + + for (int cs = 0; cs < colspan; cs++) + rowData[nextFreeCol + cs] = text; + + if (rowspan > 1) + for (int cs = 0; cs < colspan; cs++) + rowspanMap[nextFreeCol + cs] = (rowspan - 1, text); + + nextFreeCol += colspan; + } + + if (rowData.Count > 0) + { + int colCount = rowData.Keys.Max() + 1; + List row = []; + for (int c = 0; c < colCount; c++) + row.Add(rowData.TryGetValue(c, out string? cell) ? cell : string.Empty); + result.Add(row); + } + } + + pos = rowEnd; + } + + return result; + } + + private static int FindNextFreeColumnRange( + IReadOnlyDictionary rowData, + int startColumn, + int columnCount) + { + int candidate = Math.Max(0, startColumn); + + while (true) + { + bool foundOccupiedColumn = false; + for (int offset = 0; offset < columnCount; offset++) + { + if (!rowData.ContainsKey(candidate + offset)) + continue; + + candidate += offset + 1; + foundOccupiedColumn = true; + break; + } + + if (!foundOccupiedColumn) + return candidate; + } + } + + private static List<(string Text, int ColSpan, int RowSpan)> ParseHtmlRowCells(string rowHtml) + { + List<(string, int, int)> cells = []; + int pos = 0; + + while (pos < rowHtml.Length) + { + int tdPos = rowHtml.IndexOf("= 0 && (thPos < 0 || tdPos <= thPos)) + { + cellStart = tdPos; + endTag = ""; + } + else + { + cellStart = thPos; + endTag = ""; + } + + int openEnd = rowHtml.IndexOf('>', cellStart); + if (openEnd < 0) break; + + string tagAttributes = rowHtml[(cellStart + 3)..openEnd]; + int colspan = ParseSpanAttribute(tagAttributes, "colspan"); + int rowspan = ParseSpanAttribute(tagAttributes, "rowspan"); + + int contentStart = openEnd + 1; + int contentEnd = rowHtml.IndexOf(endTag, contentStart, StringComparison.OrdinalIgnoreCase); + contentEnd = contentEnd >= 0 ? contentEnd : rowHtml.Length; + + cells.Add((CleanHtmlCellContent(rowHtml[contentStart..contentEnd]), colspan, rowspan)); + pos = contentEnd + endTag.Length; + } + + return cells; + } + + private static int ParseSpanAttribute(string tagAttributes, string attributeName) + { + int attrPos = tagAttributes.IndexOf(attributeName, StringComparison.OrdinalIgnoreCase); + if (attrPos < 0) return 1; + + int eqPos = tagAttributes.IndexOf('=', attrPos + attributeName.Length); + if (eqPos < 0) return 1; + + int valueStart = eqPos + 1; + while (valueStart < tagAttributes.Length && tagAttributes[valueStart] is ' ' or '"' or '\'') + valueStart++; + + int valueEnd = valueStart; + while (valueEnd < tagAttributes.Length && char.IsDigit(tagAttributes[valueEnd])) + valueEnd++; + + if (valueEnd == valueStart) return 1; + + return int.TryParse(tagAttributes[valueStart..valueEnd], out int span) && span >= 1 + ? Math.Min(span, MaxHtmlTableSpan) + : 1; + } + + private static string CleanHtmlCellContent(string html) + { + if (string.IsNullOrEmpty(html)) + return string.Empty; + + html = Regex.Replace(html, @"", " ", RegexOptions.IgnoreCase); + html = Regex.Replace(html, @"<[^>]*>", string.Empty); + html = WebUtility.HtmlDecode(html); + + return html.Trim(); + } +} diff --git a/Text-Grab/Utilities/ClipboardUtilities.cs b/Text-Grab/Utilities/ClipboardUtilities.cs index f3e2d1d9..3b588588 100644 --- a/Text-Grab/Utilities/ClipboardUtilities.cs +++ b/Text-Grab/Utilities/ClipboardUtilities.cs @@ -15,8 +15,6 @@ namespace Text_Grab.Utilities; public class ClipboardUtilities { - private const int MaxHtmlTableSpan = 16_384; - public static async Task<(bool, string)> TryGetClipboardText() { DataPackageView? dataPackageView = null; @@ -61,7 +59,7 @@ public static (bool, ImageSource?) TryGetImageFromClipboard() { IDataObject? clipboardData = System.Windows.Clipboard.GetDataObject(); if (clipboardData is null - || !clipboardData.GetDataPresent(System.Windows.Forms.DataFormats.Bitmap)) + || !clipboardData.GetDataPresent(System.Windows.DataFormats.Bitmap)) return (false, null); imageSource = System.Windows.Clipboard.GetImage(); @@ -133,55 +131,6 @@ private static string CleanTeamsBase64Image(string dirtyTeamsString) return sb.ToString(); } - public static string BuildCfHtmlTable(IReadOnlyList> rows) - { - if (rows is null || rows.Count == 0) - return string.Empty; - - StringBuilder table = new(); - table.Append(""); - - foreach (IReadOnlyList row in rows) - { - table.Append(""); - foreach (string cell in row) - { - table.Append(""); - } - table.Append(""); - } - - table.Append("
"); - table.Append(WebUtility.HtmlEncode(cell ?? string.Empty).Replace("\r\n", "
").Replace("\n", "
")); - table.Append("
"); - - return WrapHtmlFragmentAsCfHtml(table.ToString()); - } - - internal static string WrapHtmlFragmentAsCfHtml(string htmlFragment) - { - const string htmlPrefix = "\r\n\r\n"; - const string htmlSuffix = "\r\n\r\n\r\n"; - - // CF_HTML header fields are 10-digit, zero-padded byte offsets into the UTF-8 - // encoded clipboard payload. See https://learn.microsoft.com/windows/win32/dataxchg/html-clipboard-format - static string BuildHeader(int startHtml, int endHtml, int startFragment, int endFragment) => - "Version:0.9\r\n" + - $"StartHTML:{startHtml:D10}\r\n" + - $"EndHTML:{endHtml:D10}\r\n" + - $"StartFragment:{startFragment:D10}\r\n" + - $"EndFragment:{endFragment:D10}\r\n"; - - int headerByteLength = Encoding.UTF8.GetByteCount(BuildHeader(0, 0, 0, 0)); - int startHtmlOffset = headerByteLength; - int startFragmentOffset = startHtmlOffset + Encoding.UTF8.GetByteCount(htmlPrefix); - int endFragmentOffset = startFragmentOffset + Encoding.UTF8.GetByteCount(htmlFragment); - int endHtmlOffset = endFragmentOffset + Encoding.UTF8.GetByteCount(htmlSuffix); - - return BuildHeader(startHtmlOffset, endHtmlOffset, startFragmentOffset, endFragmentOffset) - + htmlPrefix + htmlFragment + htmlSuffix; - } - public static bool TryGetHtmlTableAsTabSeparated(out string tabSeparated) { tabSeparated = string.Empty; @@ -194,7 +143,7 @@ public static bool TryGetHtmlTableAsTabSeparated(out string tabSeparated) if (string.IsNullOrEmpty(htmlData)) return false; - string result = ConvertHtmlToTabSeparated(htmlData); + string result = CfHtmlTableUtilities.ConvertHtmlToTabSeparated(htmlData); if (string.IsNullOrEmpty(result)) return false; @@ -207,240 +156,6 @@ public static bool TryGetHtmlTableAsTabSeparated(out string tabSeparated) } } - internal static string ConvertHtmlToTabSeparated(string cfHtml) - { - string fragment = ExtractHtmlFragment(cfHtml); - List> table = ParseHtmlTableToGrid(fragment); - if (table.Count == 0) - return string.Empty; - - StringBuilder sb = new(); - for (int r = 0; r < table.Count; r++) - { - if (r > 0) sb.Append('\n'); - sb.Append(string.Join("\t", table[r])); - } - return sb.ToString(); - } - - private static string ExtractHtmlFragment(string cfHtml) - { - int startPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); - if (startPos < 0) - startPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); - - int endPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); - if (endPos < 0) - endPos = cfHtml.IndexOf("", StringComparison.OrdinalIgnoreCase); - - if (startPos >= 0 && endPos > startPos) - { - int fragmentStart = cfHtml.IndexOf("-->", startPos) + 3; - return cfHtml[fragmentStart..endPos]; - } - - // Fall back to byte-offset headers (StartFragment:/EndFragment:) - const string startKey = "StartFragment:"; - const string endKey = "EndFragment:"; - int sfIdx = cfHtml.IndexOf(startKey, StringComparison.OrdinalIgnoreCase); - int efIdx = cfHtml.IndexOf(endKey, StringComparison.OrdinalIgnoreCase); - - if (sfIdx >= 0 && efIdx >= 0) - { - int sfNumStart = sfIdx + startKey.Length; - int sfLineEnd = cfHtml.IndexOf('\n', sfNumStart); - int efNumStart = efIdx + endKey.Length; - int efLineEnd = cfHtml.IndexOf('\n', efNumStart); - - if (sfLineEnd > sfNumStart && efLineEnd > efNumStart - && int.TryParse(cfHtml[sfNumStart..sfLineEnd].Trim(), out int sfOff) - && int.TryParse(cfHtml[efNumStart..efLineEnd].Trim(), out int efOff) - && sfOff >= 0 && efOff > sfOff && efOff <= cfHtml.Length) - { - return cfHtml[sfOff..efOff]; - } - } - - return cfHtml; - } - - private static List> ParseHtmlTableToGrid(string html) - { - List> result = []; - int tableStart = html.IndexOf("", StringComparison.OrdinalIgnoreCase); - tableEnd = tableEnd >= 0 ? tableEnd + 8 : html.Length; - - string tableHtml = html[tableStart..tableEnd]; - - // Tracks cells that span into future rows: col -> (remaining rows to fill, cell content) - Dictionary rowspanMap = []; - - int pos = 0; - while (pos < tableHtml.Length) - { - int rowStart = tableHtml.IndexOf("", rowStart, StringComparison.OrdinalIgnoreCase); - rowEnd = rowEnd >= 0 ? rowEnd + 5 : tableHtml.Length; - - List<(string Text, int ColSpan, int RowSpan)> parsedCells = - ParseHtmlRowCells(tableHtml[rowStart..rowEnd]); - - if (parsedCells.Count > 0 || rowspanMap.Count > 0) - { - // Build a sparse column map for this row - Dictionary rowData = []; - - // Apply rowspan carry-overs from previous rows first - foreach (int col in rowspanMap.Keys.OrderBy(k => k).ToList()) - { - (int rem, string content) = rowspanMap[col]; - rowData[col] = content; - if (rem > 1) - rowspanMap[col] = (rem - 1, content); - else - rowspanMap.Remove(col); - } - - // Place each parsed cell in the next free column(s) - int nextFreeCol = 0; - foreach ((string text, int colspan, int rowspan) in parsedCells) - { - nextFreeCol = FindNextFreeColumnRange(rowData, nextFreeCol, colspan); - - for (int cs = 0; cs < colspan; cs++) - rowData[nextFreeCol + cs] = text; - - if (rowspan > 1) - for (int cs = 0; cs < colspan; cs++) - rowspanMap[nextFreeCol + cs] = (rowspan - 1, text); - - nextFreeCol += colspan; - } - - if (rowData.Count > 0) - { - int colCount = rowData.Keys.Max() + 1; - List row = []; - for (int c = 0; c < colCount; c++) - row.Add(rowData.TryGetValue(c, out string? cell) ? cell : string.Empty); - result.Add(row); - } - } - - pos = rowEnd; - } - - return result; - } - - private static int FindNextFreeColumnRange( - IReadOnlyDictionary rowData, - int startColumn, - int columnCount) - { - int candidate = Math.Max(0, startColumn); - - while (true) - { - bool foundOccupiedColumn = false; - for (int offset = 0; offset < columnCount; offset++) - { - if (!rowData.ContainsKey(candidate + offset)) - continue; - - candidate += offset + 1; - foundOccupiedColumn = true; - break; - } - - if (!foundOccupiedColumn) - return candidate; - } - } - - private static List<(string Text, int ColSpan, int RowSpan)> ParseHtmlRowCells(string rowHtml) - { - List<(string, int, int)> cells = []; - int pos = 0; - - while (pos < rowHtml.Length) - { - int tdPos = rowHtml.IndexOf("= 0 && (thPos < 0 || tdPos <= thPos)) - { - cellStart = tdPos; - endTag = ""; - } - else - { - cellStart = thPos; - endTag = ""; - } - - int openEnd = rowHtml.IndexOf('>', cellStart); - if (openEnd < 0) break; - - string tagAttributes = rowHtml[(cellStart + 3)..openEnd]; - int colspan = ParseSpanAttribute(tagAttributes, "colspan"); - int rowspan = ParseSpanAttribute(tagAttributes, "rowspan"); - - int contentStart = openEnd + 1; - int contentEnd = rowHtml.IndexOf(endTag, contentStart, StringComparison.OrdinalIgnoreCase); - contentEnd = contentEnd >= 0 ? contentEnd : rowHtml.Length; - - cells.Add((CleanHtmlCellContent(rowHtml[contentStart..contentEnd]), colspan, rowspan)); - pos = contentEnd + endTag.Length; - } - - return cells; - } - - private static int ParseSpanAttribute(string tagAttributes, string attributeName) - { - int attrPos = tagAttributes.IndexOf(attributeName, StringComparison.OrdinalIgnoreCase); - if (attrPos < 0) return 1; - - int eqPos = tagAttributes.IndexOf('=', attrPos + attributeName.Length); - if (eqPos < 0) return 1; - - int valueStart = eqPos + 1; - while (valueStart < tagAttributes.Length && tagAttributes[valueStart] is ' ' or '"' or '\'') - valueStart++; - - int valueEnd = valueStart; - while (valueEnd < tagAttributes.Length && char.IsDigit(tagAttributes[valueEnd])) - valueEnd++; - - if (valueEnd == valueStart) return 1; - - return int.TryParse(tagAttributes[valueStart..valueEnd], out int span) && span >= 1 - ? Math.Min(span, MaxHtmlTableSpan) - : 1; - } - - private static string CleanHtmlCellContent(string html) - { - if (string.IsNullOrEmpty(html)) - return string.Empty; - - html = Regex.Replace(html, @"", " ", RegexOptions.IgnoreCase); - html = Regex.Replace(html, @"<[^>]*>", string.Empty); - html = WebUtility.HtmlDecode(html); - - return html.Trim(); - } - private static string base64ImageExtension(ref string base64String) { // Copied this portion of the code from https://github.com/veler/DevToys diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index 090d524e..d4bba955 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -1544,7 +1544,7 @@ internal static string BuildSpreadsheetSelectionHtml( .Select(cell => dataTable.Rows[cell.RowIndex][cell.ColumnIndex]?.ToString() ?? string.Empty) .ToList())]; - return ClipboardUtilities.BuildCfHtmlTable(rows); + return CfHtmlTableUtilities.BuildCfHtmlTable(rows); } internal static string BuildSpreadsheetSelectionMarkdown( From bb5484c42d797ddee903f3ba18aa4c663ffa9368 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:26:57 -0500 Subject: [PATCH 34/54] Split FreeformCaptureUtilities.cs, move CreateMaskedBitmap to Core.Windows Batch 5e of the Core split. Call-site census before naming: GetBounds and BuildGeometry each have 3 external callers (Tests/FreeformCaptureUtilitiesTests.cs plus two call sites in FullscreenGrab.SelectionStyles.cs) and return WPF rendering types (Rect, PathGeometry), so they stay in the app under the original FreeformCaptureUtilities name. CreateMaskedBitmap has 2 (the same test file, one call site) - the minority - so it took a new name, Text-Grab.Core.Windows/Utilities/ BitmapMaskUtilities.cs, same as 5c's direction. Keeping both classes named FreeformCaptureUtilities in the same Text_Grab.Utilities namespace across two assemblies the app references would have been a straight CS0433 collision, not just a style choice. CreateMaskedBitmap's parameter changed from IReadOnlyList to IReadOnlyList - GDI+'s GraphicsPath.AddPolygon already wanted PointF internally, so the method previously converted Point->PointF itself; now the caller does. The single call site, FullscreenGrab.SelectionStyles.cs's CreateFreeformSelectionResult, converts via the existing ShapeExtensions.AsPointF() (added in the B2 geometry work) before calling in. FreeformCaptureUtilitiesTests.cs's CreateMaskedBitmap test updated the same way, constructing PointF literals directly. Self-assembly-reflection audit: grepped BitmapMaskUtilities.cs for GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and AppContext.BaseDirectory - zero matches. No files deferred from this batch's list; the split moved cleanly. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean (0 errors; pre-existing MSB3277 assembly-conflict warnings only). Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - identical to baseline, total 1510 conserved. Co-Authored-By: Claude Opus 5 --- Tests/FreeformCaptureUtilitiesTests.cs | 10 +++--- .../Utilities/BitmapMaskUtilities.cs | 35 +++++++++++++++++++ .../Utilities/FreeformCaptureUtilities.cs | 21 ----------- .../Views/FullscreenGrab.SelectionStyles.cs | 3 +- 4 files changed, 42 insertions(+), 27 deletions(-) create mode 100644 Text-Grab.Core.Windows/Utilities/BitmapMaskUtilities.cs diff --git a/Tests/FreeformCaptureUtilitiesTests.cs b/Tests/FreeformCaptureUtilitiesTests.cs index 3cf860ff..91bc624c 100644 --- a/Tests/FreeformCaptureUtilitiesTests.cs +++ b/Tests/FreeformCaptureUtilitiesTests.cs @@ -48,13 +48,13 @@ public void CreateMaskedBitmap_WhitensPixelsOutsideThePolygon() using Graphics graphics = Graphics.FromImage(sourceBitmap); graphics.Clear(System.Drawing.Color.Black); - using Bitmap maskedBitmap = FreeformCaptureUtilities.CreateMaskedBitmap( + using Bitmap maskedBitmap = BitmapMaskUtilities.CreateMaskedBitmap( sourceBitmap, [ - new Point(2, 2), - new Point(7, 2), - new Point(7, 7), - new Point(2, 7) + new PointF(2, 2), + new PointF(7, 2), + new PointF(7, 7), + new PointF(2, 7) ]); Assert.Equal(System.Drawing.Color.Gray.ToArgb(), maskedBitmap.GetPixel(0, 0).ToArgb()); diff --git a/Text-Grab.Core.Windows/Utilities/BitmapMaskUtilities.cs b/Text-Grab.Core.Windows/Utilities/BitmapMaskUtilities.cs new file mode 100644 index 00000000..a690cc97 --- /dev/null +++ b/Text-Grab.Core.Windows/Utilities/BitmapMaskUtilities.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace Text_Grab.Utilities; + +/// +/// Split out of FreeformCaptureUtilities: the one member of that file with no WPF rendering +/// type in its signature. GetBounds and BuildGeometry return System.Windows.Rect/PathGeometry +/// and stay in the app. +/// +public static class BitmapMaskUtilities +{ + public static Bitmap CreateMaskedBitmap(Bitmap sourceBitmap, IReadOnlyList pointsRelativeToBounds) + { + ArgumentNullException.ThrowIfNull(sourceBitmap); + + if (pointsRelativeToBounds is null || pointsRelativeToBounds.Count < 3) + return new Bitmap(sourceBitmap); + + Bitmap maskedBitmap = new(sourceBitmap.Width, sourceBitmap.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using Graphics graphics = Graphics.FromImage(maskedBitmap); + using GraphicsPath graphicsPath = new(); + + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.Clear(System.Drawing.Color.Gray); + + graphicsPath.AddPolygon([.. pointsRelativeToBounds]); + graphics.SetClip(graphicsPath); + graphics.DrawImage(sourceBitmap, new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height)); + + return maskedBitmap; + } +} diff --git a/Text-Grab/Utilities/FreeformCaptureUtilities.cs b/Text-Grab/Utilities/FreeformCaptureUtilities.cs index 02383864..81aae920 100644 --- a/Text-Grab/Utilities/FreeformCaptureUtilities.cs +++ b/Text-Grab/Utilities/FreeformCaptureUtilities.cs @@ -46,25 +46,4 @@ public static PathGeometry BuildGeometry(IReadOnlyList points) geometry.Freeze(); return geometry; } - - public static Bitmap CreateMaskedBitmap(Bitmap sourceBitmap, IReadOnlyList pointsRelativeToBounds) - { - ArgumentNullException.ThrowIfNull(sourceBitmap); - - if (pointsRelativeToBounds is null || pointsRelativeToBounds.Count < 3) - return new Bitmap(sourceBitmap); - - Bitmap maskedBitmap = new(sourceBitmap.Width, sourceBitmap.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - using Graphics graphics = Graphics.FromImage(maskedBitmap); - using GraphicsPath graphicsPath = new(); - - graphics.SmoothingMode = SmoothingMode.AntiAlias; - graphics.Clear(System.Drawing.Color.Gray); - - graphicsPath.AddPolygon([.. pointsRelativeToBounds.Select(static point => new PointF((float)point.X, (float)point.Y))]); - graphics.SetClip(graphicsPath); - graphics.DrawImage(sourceBitmap, new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height)); - - return maskedBitmap; - } } diff --git a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs index 76f02ec1..c52fcc89 100644 --- a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs +++ b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs @@ -847,7 +847,8 @@ private FullscreenCaptureResult CreateRectangleSelectionResult(FsgSelectionStyle List relativePoints = [.. devicePoints.Select(point => new Point(point.X - deviceBounds.X, point.Y - deviceBounds.Y))]; using Bitmap rawBitmap = ImageMethods.GetRegionOfScreenAsBitmap(absoluteCaptureRect.AsRectangle(), cacheResult: false); - Bitmap maskedBitmap = FreeformCaptureUtilities.CreateMaskedBitmap(rawBitmap, relativePoints); + List relativePointsF = [.. relativePoints.Select(point => point.AsPointF())]; + Bitmap maskedBitmap = BitmapMaskUtilities.CreateMaskedBitmap(rawBitmap, relativePointsF); Singleton.Instance.CacheLastBitmap(maskedBitmap); BitmapSource captureImage = ImageMethods.BitmapToImageSource(maskedBitmap); From 5ad496859c87237af7c60e1e70660325cf9afc0b Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:28:48 -0500 Subject: [PATCH 35/54] Drop the dead ToSourceAsync and the WinUI package it pulled in Follow-up to batch 5a. SoftwareBitmapExtensions.ToSourceAsync returns a Microsoft.UI.Xaml.Media.Imaging.SoftwareBitmapSource, which is why 5a added a Microsoft.WindowsAppSDK.WinUI PackageReference to Text-Grab.Core.Windows to get the file to compile there. The method has zero call sites - anywhere, in any project, including XAML. The file carries a Microsoft sample header; ToSourceAsync is the stock WinUI sample helper, copied in with the rest and never used. Text-Grab is a WPF app and has no WinUI surface for a SoftwareBitmapSource to bind to. So the package was being added to a Core project purely to satisfy dead code, and at a version (2.3.6) that did not even match the WindowsAppSDK.AI reference already there (2.4.4). Deleted the method, its using, and the PackageReference. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, unchanged. Co-Authored-By: Claude Opus 5 --- .../Extensions/SoftwareBitmapExtensions.cs | 18 ------------------ .../Text-Grab.Core.Windows.csproj | 2 -- 2 files changed, 20 deletions(-) diff --git a/Text-Grab.Core.Windows/Extensions/SoftwareBitmapExtensions.cs b/Text-Grab.Core.Windows/Extensions/SoftwareBitmapExtensions.cs index ceea81ac..1d5eb70e 100644 --- a/Text-Grab.Core.Windows/Extensions/SoftwareBitmapExtensions.cs +++ b/Text-Grab.Core.Windows/Extensions/SoftwareBitmapExtensions.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.UI.Xaml.Media.Imaging; using System; using System.Drawing.Imaging; using System.IO; @@ -14,23 +13,6 @@ namespace Text_Grab.Extensions; public static class SoftwareBitmapExtensions { - public static async Task ToSourceAsync(this SoftwareBitmap softwareBitmap) - { - SoftwareBitmapSource source = new(); - - if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 || softwareBitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied) - { - SoftwareBitmap convertedBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); - await source.SetBitmapAsync(convertedBitmap); - } - else - { - await source.SetBitmapAsync(softwareBitmap); - } - - return source; - } - public static async Task FilePathToSoftwareBitmapAsync(this string filePath) { using IRandomAccessStream stream = await StorageFileExtensions.CreateStreamAsync(filePath); diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index f5f64348..cd5ec575 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -46,8 +46,6 @@ - - From f156b639828e3836dec97ce802df115b9fb952b2 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:32:27 -0500 Subject: [PATCH 36/54] Move HdrScreenCapture into Core.Windows behind a UI-thread hook Batch 5b of the Core split, plus 5c's deferred CaptureScreenRegion, which this unblocks. Two blockers, as section 4.5 predicted. HdrBorderlessGranted and HdrCaptureCorrection joined ITextGrabSettings. Both already existed in Settings.settings, so the generated partial satisfies them with no .settings edit and no forwarding. The Application.Current.Dispatcher hop became Text-Grab.Core/Services/ UiThreadAccess.cs, the same delegate-resolver shape as SettingsAccess and for the same reason - Dispatcher lives in WindowsBase, which Core cannot see. The app registers a poster from a [ModuleInitializer], mirroring SettingsAccessInitializer, so the Tests host is covered without an App.appStartup call. A delegate rather than a stored dispatcher matters here: Application.Current is null at module-load time and only becomes non-null once WPF starts, so the poster resolves it on each call. That preserves the original late binding exactly. TryPost returning false is the old `dispatcher is null` branch - nothing to post to, nothing happens - and _borderlessRequestStarted is still set before the post either way, so a process with no UI thread does not re-fire the consent request on every capture. With HdrScreenCapture in Core.Windows, CaptureScreenRegion could follow. It moved into BitmapUtilities as internal rather than public: its only two callers, GetRegionOfScreenAsBitmap and GetWindowsBoundsBitmap, stay in the app - the first writes to HistoryService, the second pattern-matches on the GrabFrame view - and Core.Windows already grants InternalsVisibleTo to Text-Grab. Its section 7 row is removed. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in any file touched. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, conserved. Co-Authored-By: Claude Opus 5 --- .../Utilities/BitmapUtilities.cs | 26 ++++++++++ .../Utilities/Hdr/HdrScreenCapture.cs | 13 ++--- .../Interfaces/ITextGrabSettings.cs | 6 +++ Text-Grab.Core/Services/UiThreadAccess.cs | 50 +++++++++++++++++++ Text-Grab/Utilities/ImageMethods.cs | 20 +------- .../Utilities/UiThreadAccessInitializer.cs | 26 ++++++++++ docs/Core-Split-Plan.md | 15 +++++- 7 files changed, 131 insertions(+), 25 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/Hdr/HdrScreenCapture.cs (97%) create mode 100644 Text-Grab.Core/Services/UiThreadAccess.cs create mode 100644 Text-Grab/Utilities/UiThreadAccessInitializer.cs diff --git a/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs b/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs index e27b4ba1..eabd7a58 100644 --- a/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs @@ -2,6 +2,8 @@ using System.Drawing; using System.IO; using Text_Grab.Extensions; +using Text_Grab.Services; +using Text_Grab.Utilities.Hdr; using Windows.Storage.Streams; namespace Text_Grab; @@ -42,4 +44,28 @@ internal static RotateFlipType GetRotateFlipType(string path) RotateFlipType rotateFlipType = img.GetRotateFlipType(); return rotateFlipType; } + + /// + /// Grabs a virtual-desktop region as a bitmap, preferring the HDR-aware capture path when the + /// user has enabled it. Internal rather than public: its only callers are ImageMethods' + /// GetRegionOfScreenAsBitmap and GetWindowsBoundsBitmap, both of which stay in the app - + /// GetRegionOfScreenAsBitmap because it writes to HistoryService, GetWindowsBoundsBitmap + /// because it pattern-matches on the GrabFrame view. It was private to ImageMethods before + /// batch 5b unblocked it by moving HdrScreenCapture into this assembly. + /// + internal static Bitmap CaptureScreenRegion(Rectangle region) + { + if (SettingsAccess.Current.HdrCaptureCorrection) + { + Bitmap? hdrBitmap = HdrScreenCapture.TryCaptureRegion(region); + if (hdrBitmap is not null) + return hdrBitmap; + } + + Bitmap bmp = new(region.Width, region.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using Graphics g = Graphics.FromImage(bmp); + + g.CopyFromScreen(region.Left, region.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy); + return bmp; + } } diff --git a/Text-Grab/Utilities/Hdr/HdrScreenCapture.cs b/Text-Grab.Core.Windows/Utilities/Hdr/HdrScreenCapture.cs similarity index 97% rename from Text-Grab/Utilities/Hdr/HdrScreenCapture.cs rename to Text-Grab.Core.Windows/Utilities/Hdr/HdrScreenCapture.cs index bb5c14f6..96588196 100644 --- a/Text-Grab/Utilities/Hdr/HdrScreenCapture.cs +++ b/Text-Grab.Core.Windows/Utilities/Hdr/HdrScreenCapture.cs @@ -4,6 +4,7 @@ using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; +using Text_Grab.Services; using System.Threading; using Vortice.Direct3D; using Vortice.Direct3D11; @@ -463,17 +464,17 @@ private static void EnsureBorderlessRequestedOnce() // Only silently re-activate for users who already granted access in a past session, so a // consent prompt never appears unexpectedly during a grab. First-time consent is explicit, // via the "Check permissions" button in settings. - if (!AppUtilities.TextGrabSettings.HdrBorderlessGranted) + if (!SettingsAccess.Current.HdrBorderlessGranted) return; if (System.Threading.Interlocked.Exchange(ref _borderlessRequestStarted, 1) != 0) return; - System.Windows.Threading.Dispatcher? dispatcher = System.Windows.Application.Current?.Dispatcher; - if (dispatcher is null) - return; - - _ = dispatcher.InvokeAsync(async () => await RequestBorderlessAccessAsync()); + // Was Application.Current?.Dispatcher.InvokeAsync before this file moved out of the app. + // TryPost returning false is the old "dispatcher is null" branch: nothing to post to, so + // nothing happens. The flag above is still set either way, exactly as before - a process + // with no UI thread does not retry the request on every capture. + _ = UiThreadAccess.TryPost(static () => _ = RequestBorderlessAccessAsync()); } #region WinRT / D3D interop diff --git a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs index 6efc4962..d65563a4 100644 --- a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs +++ b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs @@ -35,6 +35,12 @@ public interface ITextGrabSettings /// Scan captured images for barcodes and QR codes. bool TryToReadBarcodes { get; set; } + /// Use the HDR-aware capture path for screen regions. + bool HdrCaptureCorrection { get; set; } + + /// Whether the user has already granted borderless screen-capture access. + bool HdrBorderlessGranted { get; set; } + /// Fall back to OCR when UI Automation returns no text. bool UiAutomationFallbackToOcr { get; set; } diff --git a/Text-Grab.Core/Services/UiThreadAccess.cs b/Text-Grab.Core/Services/UiThreadAccess.cs new file mode 100644 index 00000000..af5790cc --- /dev/null +++ b/Text-Grab.Core/Services/UiThreadAccess.cs @@ -0,0 +1,50 @@ +using System; + +namespace Text_Grab.Services; + +/// +/// How portable code gets work onto the app's UI thread. +/// +/// The same shape as , and for the same reason: Core cannot see +/// System.Windows.Application.Current.Dispatcher, which lives in WindowsBase. The app +/// registers a poster at module load and Core code calls . +/// +/// A delegate rather than a stored dispatcher because Application.Current is null at +/// module-initializer time and only becomes non-null once WPF starts. Resolving it inside the +/// registered delegate keeps the original late-bound behaviour: code that runs with no WPF +/// application - the test host, most obviously - simply finds nothing to post to. +/// +public static class UiThreadAccess +{ + private static Action? _poster; + + /// + /// Registers how to run an action on the UI thread. The app calls this from a module + /// initializer. Tests may call it again to substitute a synchronous fake. + /// + public static void SetPoster(Action poster) + => _poster = poster ?? throw new ArgumentNullException(nameof(poster)); + + /// Drops the registered poster. For tests that need to restore a clean slate. + public static void ClearPoster() => _poster = null; + + /// Whether a poster has been registered. + public static bool IsConfigured => _poster is not null; + + /// + /// Queues to run on the UI thread and returns true, or returns + /// false when there is no UI thread to post to. Callers are expected to treat false as + /// "nothing to do" rather than an error - it is the ordinary case in a headless process. + /// + public static bool TryPost(Action action) + { + ArgumentNullException.ThrowIfNull(action); + + Action? poster = _poster; + if (poster is null) + return false; + + poster(action); + return true; + } +} diff --git a/Text-Grab/Utilities/ImageMethods.cs b/Text-Grab/Utilities/ImageMethods.cs index 0e0866e6..c71e7d15 100644 --- a/Text-Grab/Utilities/ImageMethods.cs +++ b/Text-Grab/Utilities/ImageMethods.cs @@ -79,25 +79,9 @@ public static BitmapImage CachedBitmapToBitmapImage(System.Windows.Media.Imaging /// full precision and tone-map it back to SDR so the result isn't washed out (issue #111). /// Falls back to a plain GDI screen copy otherwise or if HDR capture fails. ///
- private static Bitmap CaptureScreenRegion(Rectangle region) - { - if (AppUtilities.TextGrabSettings.HdrCaptureCorrection) - { - Bitmap? hdrBitmap = HdrScreenCapture.TryCaptureRegion(region); - if (hdrBitmap is not null) - return hdrBitmap; - } - - Bitmap bmp = new(region.Width, region.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - using Graphics g = Graphics.FromImage(bmp); - - g.CopyFromScreen(region.Left, region.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy); - return bmp; - } - public static Bitmap GetRegionOfScreenAsBitmap(Rectangle region, bool cacheResult = true) { - Bitmap bmp = CaptureScreenRegion(region); + Bitmap bmp = BitmapUtilities.CaptureScreenRegion(region); bmp = BitmapUtilities.PadImage(bmp); if (cacheResult) @@ -144,7 +128,7 @@ public static Bitmap GetWindowsBoundsBitmap(Window passedWindow) } Rectangle windowRegion = new(thisCorrectedLeft, thisCorrectedTop, windowWidth, windowHeight); - return CaptureScreenRegion(windowRegion); + return BitmapUtilities.CaptureScreenRegion(windowRegion); } public static ImageSource GetWindowBoundsImage(Window passedWindow) diff --git a/Text-Grab/Utilities/UiThreadAccessInitializer.cs b/Text-Grab/Utilities/UiThreadAccessInitializer.cs new file mode 100644 index 00000000..c37289a1 --- /dev/null +++ b/Text-Grab/Utilities/UiThreadAccessInitializer.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; +using Text_Grab.Services; + +namespace Text_Grab.Utilities; + +/// +/// Points Text-Grab.Core's at the WPF dispatcher. +/// +internal static class UiThreadAccessInitializer +{ + /// + /// Same reasoning as : a module initializer rather + /// than a call in App.appStartup, so the Tests host is covered too. + /// + /// Application.Current is read inside the delegate, not here - at module-load time it + /// is still null. When it is null at call time the post is simply dropped, which is what the + /// code this replaced did with its dispatcher is null check. + /// + [ModuleInitializer] + internal static void Initialize() + => UiThreadAccess.SetPoster(static action => + { + System.Windows.Threading.Dispatcher? dispatcher = System.Windows.Application.Current?.Dispatcher; + _ = dispatcher?.InvokeAsync(action); + }); +} diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index f9845229..4ce9604e 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -443,6 +443,20 @@ still app-side, deferred on `SoftwareBitmapExtensions` - which is 5a. Run 4e aft hop at line 472 behind a settable hook the app wires up at startup — it exists to pump a one-time OS consent dialog and is load-bearing. +**5b as executed.** Both blockers cleared. `HdrBorderlessGranted` and `HdrCaptureCorrection` were +added to `ITextGrabSettings`; both already existed in `Settings.settings`, so neither needed a +`.settings` edit. The `Application.Current.Dispatcher` hop became +`Text-Grab.Core/Services/UiThreadAccess.cs` - the same delegate-resolver shape as `SettingsAccess`, +registered from an app-side `[ModuleInitializer]` so the Tests host is covered without an +`App.appStartup` call. `TryPost` returning false is exactly the old `dispatcher is null` branch, +and `_borderlessRequestStarted` is still set before the post either way, so a process with no UI +thread does not re-request on every capture. + +With `HdrScreenCapture` in Core.Windows, 5c's deferred `CaptureScreenRegion` moved as well - into +`BitmapUtilities` as `internal`, since its only two callers (`GetRegionOfScreenAsBitmap`, +`GetWindowsBoundsBitmap`) stay in the app and Core.Windows already grants `InternalsVisibleTo` +to it. That row is out of section 7. + **5c — `ImageMethods.cs` split.** Headless half (→ Core.Windows): `PadImage`, `CaptureScreenRegion`, `GetBitmapFromIRandomAccessStream`, `GetRotateFlipType(string)`. Everything touching `BitmapImage`/`BitmapSource`/`CachedBitmap`/`InteropBitmap`/`Window`/`ImageSource` stays. @@ -699,7 +713,6 @@ Every row below was verified by reading the file, not inferred. | `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam. `GrabTemplate`/`TemplateRegion` moved to Core in 2d and 4c has landed, so the remaining blocker is the settings façade plus its calls into `OcrSourceUtilities` | a façade + `OcrSourceUtilities` | | `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | | `Services/HistoryService.cs` | Headless JSON pipeline interleaved with WPF menu building and `GrabFrame`/`EditTextWindow` construction, sharing private state | Opus split (6e) | -| `Utilities/ImageMethods.cs: CaptureScreenRegion` | Calls `HdrScreenCapture.TryCaptureRegion`, which stays in the app pending 5b's settable dispatcher hook. Core.Windows cannot reference the app, so this cannot move until `HdrScreenCapture.cs` does. `PadImage`, `GetBitmapFromIRandomAccessStream` and `GetRotateFlipType(string)` moved on their own to `Text-Grab.Core.Windows/Utilities/BitmapUtilities.cs`; `HdrCaptureCorrection` was **not** added to `ITextGrabSettings` because nothing that actually moved reads it - `CaptureScreenRegion` still reads it via `AppUtilities.TextGrabSettings` in the app | 5b | ## 8. Verified dead code — free, zero-risk prep From f1032946ae69dff1f5ca9051728186235801c13f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:36:04 -0500 Subject: [PATCH 37/54] Move the Windows AI and language chain into Core.Windows Batch 4e of the Core split, run after wave 5 rather than inside wave 4 - the reason is recorded in section 4.4. WindowsAiUtilities went first, because LanguageService calls it. All three of its deferred blockers were gone: AutomationProfile landed in Core in 6a, SoftwareBitmapExtensions in 5a, and OverrideAiArchCheck is added to ITextGrabSettings here (it already existed in Settings.settings). It read that one through Settings.Default rather than AppUtilities.TextGrabSettings, which is the same object in production - SettingsService.ClassicSettings is constructed from Properties.Settings.Default - so routing it through SettingsAccess is identical at runtime and consistent with every other settings read in the moved code. Its remaining app call, AppUtilities.IsPackaged(), is a one-line forwarder to PackageIdentity.IsPackaged(), which has been in Core.Windows since 1b. The language chain then moved unsplit - LanguageService, LanguageUtilities and CaptureLanguageUtilities, all keeping their names - so none of their 155 call sites needed an edit. UiAutomationEnabled and WindowsAiDescriptionEnabled joined ITextGrabSettings as the consolidated table predicted. Singleton was already in Core. The InputLanguageManager blocker became Text-Grab.Core/Services/ InputLanguageAccess.cs, the third use of the delegate-resolver shape after SettingsAccess and UiThreadAccess. The NullReferenceException catch that guarded the read stayed on the app side, inside the registered resolver - that is the only side that knows InputLanguageManager exists, and the manager throws it from its own internals in some hosts. A null tag, whether from no resolver or no input language, still falls through to CultureInfo.CurrentUICulture and then to en-US, exactly as before. The plan proposed extracting the four pure switch helpers and leaving the input-language reader behind. That turned out to be unnecessary: the reader was the only thing in the class that was not already portable. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in any of the four moved files. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, conserved. Co-Authored-By: Claude Opus 5 --- .../Services/LanguageService.cs | 25 +++++------- .../Utilities/CaptureLanguageUtilities.cs | 7 ++-- .../Utilities/LanguageUtilities.cs | 0 .../Utilities/WindowsAiUtilities.cs | 6 +-- .../Interfaces/ITextGrabSettings.cs | 9 +++++ .../Services/InputLanguageAccess.cs | 40 +++++++++++++++++++ .../InputLanguageAccessInitializer.cs | 34 ++++++++++++++++ docs/Core-Split-Plan.md | 20 +++++++++- 8 files changed, 118 insertions(+), 23 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Services/LanguageService.cs (95%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/CaptureLanguageUtilities.cs (95%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/LanguageUtilities.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/WindowsAiUtilities.cs (98%) create mode 100644 Text-Grab.Core/Services/InputLanguageAccess.cs create mode 100644 Text-Grab/Utilities/InputLanguageAccessInitializer.cs diff --git a/Text-Grab/Services/LanguageService.cs b/Text-Grab.Core.Windows/Services/LanguageService.cs similarity index 95% rename from Text-Grab/Services/LanguageService.cs rename to Text-Grab.Core.Windows/Services/LanguageService.cs index ae0a40e0..c60364ec 100644 --- a/Text-Grab/Services/LanguageService.cs +++ b/Text-Grab.Core.Windows/Services/LanguageService.cs @@ -3,8 +3,8 @@ using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Windows.Input; using Text_Grab.Interfaces; +using Text_Grab.Services; using Text_Grab.Models; using Text_Grab.Utilities; using Windows.Globalization; @@ -76,7 +76,7 @@ public IList GetAllLanguages() List languages = []; - if (AppUtilities.TextGrabSettings.UiAutomationEnabled) + if (SettingsAccess.Current.UiAutomationEnabled) languages.Add(_uiAutomationLangInstance); if (WindowsAiUtilities.CanDeviceUseWinAI()) @@ -85,7 +85,7 @@ public IList GetAllLanguages() languages.Add(_windowsAiLangInstance); } - if (AppUtilities.TextGrabSettings.WindowsAiDescriptionEnabled + if (SettingsAccess.Current.WindowsAiDescriptionEnabled && WindowsAiUtilities.CanDeviceDescribeImagesWithWinAI()) { languages.Add(_windowsAiDescriptionLangInstance); @@ -167,7 +167,7 @@ public static (string LanguageTag, LanguageKind LanguageKind, bool UsedUiAutomat ///
public ILanguage GetOCRLanguage() { - string lastUsedLang = AppUtilities.TextGrabSettings.LastUsedLang; + string lastUsedLang = SettingsAccess.Current.LastUsedLang; lock (_cacheLock) { @@ -194,7 +194,7 @@ public ILanguage GetOCRLanguage() } else if (lastUsedLang == _windowsAiDescriptionLangTag) { - if (AppUtilities.TextGrabSettings.WindowsAiDescriptionEnabled + if (SettingsAccess.Current.WindowsAiDescriptionEnabled && WindowsAiUtilities.CanDeviceDescribeImagesWithWinAI()) { _cachedOcrLanguage = _windowsAiDescriptionLangInstance; @@ -203,7 +203,7 @@ public ILanguage GetOCRLanguage() selectedLanguage = GetCurrentInputLanguage(); } - else if (lastUsedLang == _uiAutomationLangTag && AppUtilities.TextGrabSettings.UiAutomationEnabled) + else if (lastUsedLang == _uiAutomationLangTag && SettingsAccess.Current.UiAutomationEnabled) { _cachedOcrLanguage = _uiAutomationLangInstance; return _cachedOcrLanguage; @@ -370,15 +370,10 @@ public void InvalidateAllCaches() private static string GetCurrentInputLanguageTag() { - string? currentInputLangTag = null; - try - { - currentInputLangTag = InputLanguageManager.Current?.CurrentInputLanguage?.Name; - } - catch (NullReferenceException) - { - currentInputLangTag = null; - } + // Was InputLanguageManager.Current?.CurrentInputLanguage?.Name before this file moved out + // of the app. InputLanguageManager is PresentationCore; the app registers the read (and + // owns the NullReferenceException its internals can throw) via InputLanguageAccess. + string? currentInputLangTag = InputLanguageAccess.CurrentTag; if (!string.IsNullOrWhiteSpace(currentInputLangTag)) return currentInputLangTag; diff --git a/Text-Grab/Utilities/CaptureLanguageUtilities.cs b/Text-Grab.Core.Windows/Utilities/CaptureLanguageUtilities.cs similarity index 95% rename from Text-Grab/Utilities/CaptureLanguageUtilities.cs rename to Text-Grab.Core.Windows/Utilities/CaptureLanguageUtilities.cs index bed65194..862aec51 100644 --- a/Text-Grab/Utilities/CaptureLanguageUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/CaptureLanguageUtilities.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Text_Grab.Interfaces; using Text_Grab.Models; +using Text_Grab.Services; namespace Text_Grab.Utilities; @@ -21,7 +22,7 @@ public static async Task> GetCaptureLanguagesAsync(bool includeT List languages = [.. LanguageUtilities.GetAllLanguages()]; if (includeTesseract - && AppUtilities.TextGrabSettings.UseTesseract + && SettingsAccess.Current.UseTesseract && TesseractHelper.CanLocateTesseractExe()) { List tesseractLanguages = await TesseractHelper.TesseractLanguages(); @@ -64,8 +65,8 @@ public static int FindPreferredLanguageIndex(IReadOnlyList languages, public static void PersistSelectedLanguage(ILanguage language) { - AppUtilities.TextGrabSettings.LastUsedLang = language.LanguageTag; - AppUtilities.TextGrabSettings.Save(); + SettingsAccess.Current.LastUsedLang = language.LanguageTag; + SettingsAccess.Current.Save(); LanguageUtilities.InvalidateOcrLanguageCache(); } diff --git a/Text-Grab/Utilities/LanguageUtilities.cs b/Text-Grab.Core.Windows/Utilities/LanguageUtilities.cs similarity index 100% rename from Text-Grab/Utilities/LanguageUtilities.cs rename to Text-Grab.Core.Windows/Utilities/LanguageUtilities.cs diff --git a/Text-Grab/Utilities/WindowsAiUtilities.cs b/Text-Grab.Core.Windows/Utilities/WindowsAiUtilities.cs similarity index 98% rename from Text-Grab/Utilities/WindowsAiUtilities.cs rename to Text-Grab.Core.Windows/Utilities/WindowsAiUtilities.cs index 9ae51088..f56b0a77 100644 --- a/Text-Grab/Utilities/WindowsAiUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/WindowsAiUtilities.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; using Text_Grab.Extensions; using Text_Grab.Models; -using Text_Grab.Properties; +using Text_Grab.Services; using Windows.Graphics.Imaging; namespace Text_Grab.Utilities; @@ -53,12 +53,12 @@ private static bool CanDeviceUseWinAiFeature(Func getReadyS private static bool MeetsWindowsAiPrerequisites() { // Check if the app is packaged and if the AI feature is supported - if (!AppUtilities.IsPackaged() || OSInterop.IsWindows10()) + if (!PackageIdentity.IsPackaged() || OSInterop.IsWindows10()) return false; // Today, Windows AI features are only supported on ARM64 unless overridden for debugging. Architecture arch = RuntimeInformation.ProcessArchitecture; - if (arch != Architecture.Arm64 && !Settings.Default.OverrideAiArchCheck) + if (arch != Architecture.Arm64 && !SettingsAccess.Current.OverrideAiArchCheck) return false; return true; diff --git a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs index d65563a4..0111b76e 100644 --- a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs +++ b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs @@ -26,6 +26,9 @@ public interface ITextGrabSettings /// Map look-alike Greek and Cyrillic characters to Latin. bool CorrectToLatin { get; set; } + /// Bypass the arm64 gate on the Windows AI feature checks. + bool OverrideAiArchCheck { get; set; } + /// Join OCR lines into paragraphs instead of preserving line breaks. bool ParagraphDetection { get; set; } @@ -41,6 +44,12 @@ public interface ITextGrabSettings /// Whether the user has already granted borderless screen-capture access. bool HdrBorderlessGranted { get; set; } + /// Offer UI Automation as a text source alongside the OCR engines. + bool UiAutomationEnabled { get; set; } + + /// Offer the Windows AI image-description pseudo-language. + bool WindowsAiDescriptionEnabled { get; set; } + /// Fall back to OCR when UI Automation returns no text. bool UiAutomationFallbackToOcr { get; set; } diff --git a/Text-Grab.Core/Services/InputLanguageAccess.cs b/Text-Grab.Core/Services/InputLanguageAccess.cs new file mode 100644 index 00000000..148a4228 --- /dev/null +++ b/Text-Grab.Core/Services/InputLanguageAccess.cs @@ -0,0 +1,40 @@ +using System; + +namespace Text_Grab.Services; + +/// +/// How portable code reads the user's current keyboard input language. +/// +/// The same delegate-resolver shape as and +/// . WPF's System.Windows.Input.InputLanguageManager is the +/// only source for this and lives in PresentationCore, which Core cannot see; it also throws +/// from its own internals in some hosts, so the app's +/// resolver owns that catch and simply returns null. +/// +/// Null - whether because nothing is registered or because the host has no input language - is a +/// normal answer, not an error. LanguageService falls back to +/// CultureInfo.CurrentUICulture and then to en-US, exactly as it did when it read +/// InputLanguageManager directly. +/// +public static class InputLanguageAccess +{ + private static Func? _resolver; + + /// + /// Registers the source of the current input-language tag. The app calls this from a module + /// initializer. Tests may call it again to substitute a fake. + /// + public static void SetResolver(Func resolver) + => _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + + /// Drops the registered resolver. For tests that need to restore a clean slate. + public static void ClearResolver() => _resolver = null; + + /// Whether a resolver has been registered. + public static bool IsConfigured => _resolver is not null; + + /// + /// The current input-language tag, or null when there is no resolver or no input language. + /// + public static string? CurrentTag => _resolver?.Invoke(); +} diff --git a/Text-Grab/Utilities/InputLanguageAccessInitializer.cs b/Text-Grab/Utilities/InputLanguageAccessInitializer.cs new file mode 100644 index 00000000..fc3612b8 --- /dev/null +++ b/Text-Grab/Utilities/InputLanguageAccessInitializer.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.CompilerServices; +using System.Windows.Input; +using Text_Grab.Services; + +namespace Text_Grab.Utilities; + +/// +/// Points Text-Grab.Core's at WPF's InputLanguageManager. +/// +internal static class InputLanguageAccessInitializer +{ + /// + /// Same reasoning as : a module initializer rather + /// than a call in App.appStartup, so the Tests host is covered too. + /// + /// The NullReferenceException catch came with the code from LanguageService - the manager + /// throws it from its own internals in some hosts - and stays on this side of the seam, + /// because this is the only side that knows InputLanguageManager exists. + /// + [ModuleInitializer] + internal static void Initialize() + => InputLanguageAccess.SetResolver(static () => + { + try + { + return InputLanguageManager.Current?.CurrentInputLanguage?.Name; + } + catch (NullReferenceException) + { + return null; + } + }); +} diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 4ce9604e..b7a336f2 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -426,6 +426,24 @@ to `CultureInfo.CurrentUICulture.Name` when none is registered. But `GetAllLangu `GetOCRLanguage()` both call `WindowsAiUtilities.CanDeviceUseWinAI()`, and `WindowsAiUtilities` is still app-side, deferred on `SoftwareBitmapExtensions` - which is 5a. Run 4e after wave 5. +**4e as executed, after wave 5.** `WindowsAiUtilities` moved first - its three blockers were all +gone (`AutomationProfile` in 6a, `SoftwareBitmapExtensions` in 5a, and `OverrideAiArchCheck` added +to `ITextGrabSettings` here). Its one remaining app call, `AppUtilities.IsPackaged()`, is a plain +forwarder to `PackageIdentity.IsPackaged()`, which has been in Core.Windows since 1b. + +That cleared the way for the whole language chain to move to Core.Windows **unsplit** - +`LanguageService`, `LanguageUtilities`, `CaptureLanguageUtilities`, all keeping their names, so +none of their 155 call sites needed an edit. `UiAutomationEnabled` and `WindowsAiDescriptionEnabled` +joined `ITextGrabSettings` as the table predicted. `Singleton` was already in Core. + +The `InputLanguageManager` blocker became `Text-Grab.Core/Services/InputLanguageAccess.cs`, the +third instance of the delegate-resolver shape after `SettingsAccess` and `UiThreadAccess`. The +`NullReferenceException` catch that guarded the read stayed on the app side of the seam, inside the +registered resolver, since that is the only side that knows InputLanguageManager exists. A null tag +- no resolver, or no input language - still falls through to `CultureInfo.CurrentUICulture` and then +to en-US, exactly as before. Extracting the switch helpers, which the paragraph above proposed, +turned out to be unnecessary. + ### 4.5 Wave 5 — capture and imaging → Core.Windows **5a — move-now (after Wave 1):** @@ -702,8 +720,6 @@ Every row below was verified by reading the file, not inferred. |---|---|---| | `Utilities/OcrSourceUtilities.cs` | Post-4c remainder. `LoadBitmapFromFile` builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite, and it takes `OcrAbsoluteFilePathAsync` and `OcrFile` with it. Engine dispatch additionally needs `WindowsAiUtilities` (5a) and `LanguageUtilities` (4e); the rest is `Window`/`BitmapSource` capture and stays | a GDI+/WIC rewrite, then 5a + 4e | | `Utilities/TesseractHelper.cs` | `TempImagePath()` calls `AutomationProfile.GetTemporaryDirectory()`. The settings write-back is **not** a blocker — `ITextGrabSettings.Save()` already covers it | 6a | -| `Services/LanguageService.cs` | `System.Windows.Input.InputLanguageManager` (line 376), no portable substitute; reachable in production, not vestigial | Opus split (4e) | -| `Utilities/WindowsAiUtilities.cs` | `AutomationProfile.GetTemporaryFilePath()` (109, 189); `SoftwareBitmapExtensions` unmoved; reads `Settings.Default.OverrideAiArchCheck` directly (61) | 6a + 5a + one interface add | | `Utilities/ContextMenuUtilities.cs` | `AutomationProfile.Current`, `FileUtilities.GetExePath()`; `IoUtilities` mixes pure extension lists with WinForms/Wpf.Ui `MessageBox` calls | 1a (IoUtilities split), 5, 6a | | `Utilities/FileAssociationUtilities.cs` | `FileUtilities.GetExePath()` | Wave 5 | | `Utilities/FileUtilities.cs` | `AutomationProfile.Current` in 6 methods | 6a | From 08d6a8b3fe248e40fc2b35ddbbf388b954c87174 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:40:12 -0500 Subject: [PATCH 38/54] Move the text-to-speech chain into Text-Grab.Core / Core.Windows Batch 6b of the Core split. WindowsSpeechEngine moved to Core.Windows (WinRT SpeechSynthesizer/MediaPlayer); TtsService moved to plain Core, keeping their names. TtsService's blocker was a field initializer - `private ITtsEngine _engine = new WindowsSpeechEngine();` - naming a type that now lives one tier down. Resolved with Text-Grab.Core/Services/ TtsEngineAccess.cs, the fourth delegate-resolver shape after SettingsAccess, UiThreadAccess and InputLanguageAccess: it holds a Func factory, and TtsService's constructor calls TtsEngineAccess.CreateDefault() as its first statement, so the engine is still built at the exact moment it always was - when a TtsService is constructed, not lazily on first Speak. The app registers `static () => new WindowsSpeechEngine()` from a [ModuleInitializer] (TtsEngineAccessInitializer.cs), covering the Tests host the same way SettingsAccessInitializer does. An unregistered resolver throws InvalidOperationException, matching SettingsAccess's behaviour - unreachable in production, since the module initializer always covers it. TtsSpeakWordLimit, TtsVoiceName and TtsSpeakingRate joined ITextGrabSettings; all three already existed in Settings.settings, so no .settings edit was needed. WindowsSpeechEngine's two settings reads and TtsService's one moved from Properties.Settings.Default to SettingsAccess.Current. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in any file touched. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, conserved. Co-Authored-By: Claude Opus 5 --- .../Services/WindowsSpeechEngine.cs | 5 +- .../Interfaces/ITextGrabSettings.cs | 9 ++++ Text-Grab.Core/Services/TtsEngineAccess.cs | 50 +++++++++++++++++++ .../Services/TtsService.cs | 6 +-- .../Utilities/TtsEngineAccessInitializer.cs | 18 +++++++ docs/Core-Split-Plan.md | 14 ++++++ 6 files changed, 96 insertions(+), 6 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Services/WindowsSpeechEngine.cs (91%) create mode 100644 Text-Grab.Core/Services/TtsEngineAccess.cs rename {Text-Grab => Text-Grab.Core}/Services/TtsService.cs (96%) create mode 100644 Text-Grab/Utilities/TtsEngineAccessInitializer.cs diff --git a/Text-Grab/Services/WindowsSpeechEngine.cs b/Text-Grab.Core.Windows/Services/WindowsSpeechEngine.cs similarity index 91% rename from Text-Grab/Services/WindowsSpeechEngine.cs rename to Text-Grab.Core.Windows/Services/WindowsSpeechEngine.cs index 0e5b4bc7..a5ffa8ac 100644 --- a/Text-Grab/Services/WindowsSpeechEngine.cs +++ b/Text-Grab.Core.Windows/Services/WindowsSpeechEngine.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using Text_Grab.Interfaces; -using Text_Grab.Properties; using Windows.Media.Core; using Windows.Media.Playback; using Windows.Media.SpeechSynthesis; @@ -16,7 +15,7 @@ public async Task SpeakAsync(string text, CancellationToken ct) { using SpeechSynthesizer synthesizer = new(); - string voiceName = Settings.Default.TtsVoiceName; + string voiceName = SettingsAccess.Current.TtsVoiceName; if (!string.IsNullOrEmpty(voiceName)) { VoiceInformation? voice = SpeechSynthesizer.AllVoices @@ -25,7 +24,7 @@ public async Task SpeakAsync(string text, CancellationToken ct) synthesizer.Voice = voice; } - double speakingRate = Settings.Default.TtsSpeakingRate; + double speakingRate = SettingsAccess.Current.TtsSpeakingRate; if (speakingRate >= 0.5 && speakingRate <= 6.0) synthesizer.Options.SpeakingRate = speakingRate; diff --git a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs index 0111b76e..5a9f1bed 100644 --- a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs +++ b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs @@ -62,6 +62,15 @@ public interface ITextGrabSettings /// BCP-47 tag of the language used for the last capture. string LastUsedLang { get; set; } + /// Trim spoken text to this many words; zero or negative disables the limit. + int TtsSpeakWordLimit { get; set; } + + /// Display name of the preferred text-to-speech voice; empty selects the default. + string TtsVoiceName { get; set; } + + /// Speaking rate passed to the TTS engine; only values in [0.5, 6.0] are applied. + double TtsSpeakingRate { get; set; } + /// Persist pending changes. Backed by ApplicationSettingsBase.Save(). void Save(); } diff --git a/Text-Grab.Core/Services/TtsEngineAccess.cs b/Text-Grab.Core/Services/TtsEngineAccess.cs new file mode 100644 index 00000000..41fc100a --- /dev/null +++ b/Text-Grab.Core/Services/TtsEngineAccess.cs @@ -0,0 +1,50 @@ +using System; +using Text_Grab.Interfaces; + +namespace Text_Grab.Services; + +/// +/// How gets its default speech engine. +/// +/// The same delegate-resolver shape as , +/// and . TtsService used to construct its default engine +/// with a field initializer - private ITtsEngine _engine = new WindowsSpeechEngine(); - +/// but WindowsSpeechEngine is WinRT-only and belongs in Core.Windows, which Core cannot +/// name. The app registers a factory at module load and calls +/// from its own constructor, so the engine is still built at the same +/// moment it always was: when a TtsService is constructed, not lazily on first +/// Speak. +/// +public static class TtsEngineAccess +{ + private static Func? _resolver; + + /// + /// Registers the factory for the default TTS engine. The app calls this from a module + /// initializer, so it is in place for any entry point into the app assembly - including the + /// test host. Tests may call it again to substitute a fake. + /// + public static void SetResolver(Func resolver) + => _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + + /// Drops the registered resolver. For tests that need to restore a clean slate. + public static void ClearResolver() => _resolver = null; + + /// Whether a resolver has been registered. + public static bool IsConfigured => _resolver is not null; + + /// + /// Builds a new default engine. + /// + /// + /// No resolver was registered. In the app this cannot happen - the module initializer covers + /// it. It means Core code is constructing a with neither the app + /// assembly loaded nor a test fake installed, so the caller has to supply one. + /// + public static ITtsEngine CreateDefault() + => _resolver is not null + ? _resolver() + : throw new InvalidOperationException( + $"No TTS engine resolver registered. Call {nameof(TtsEngineAccess)}.{nameof(SetResolver)} " + + "before constructing TtsService from Core code."); +} diff --git a/Text-Grab/Services/TtsService.cs b/Text-Grab.Core/Services/TtsService.cs similarity index 96% rename from Text-Grab/Services/TtsService.cs rename to Text-Grab.Core/Services/TtsService.cs index 2b970b88..b8c1bbb4 100644 --- a/Text-Grab/Services/TtsService.cs +++ b/Text-Grab.Core/Services/TtsService.cs @@ -3,13 +3,12 @@ using System.Threading.Channels; using System.Threading.Tasks; using Text_Grab.Interfaces; -using Text_Grab.Properties; namespace Text_Grab.Services; public class TtsService { - private ITtsEngine _engine = new WindowsSpeechEngine(); + private ITtsEngine _engine; private readonly Channel _queue = Channel.CreateUnbounded(); private readonly CancellationTokenSource _cts = new(); private CancellationTokenSource _speechCts = new(); @@ -37,6 +36,7 @@ public ITtsEngine Engine public TtsService() { + _engine = TtsEngineAccess.CreateDefault(); _ = Task.Run(DrainLoopAsync); } @@ -110,7 +110,7 @@ void handler() private static string ApplyWordLimit(string text) { - int wordLimit = Settings.Default.TtsSpeakWordLimit; + int wordLimit = SettingsAccess.Current.TtsSpeakWordLimit; if (wordLimit <= 0) return text; diff --git a/Text-Grab/Utilities/TtsEngineAccessInitializer.cs b/Text-Grab/Utilities/TtsEngineAccessInitializer.cs new file mode 100644 index 00000000..5b9dac39 --- /dev/null +++ b/Text-Grab/Utilities/TtsEngineAccessInitializer.cs @@ -0,0 +1,18 @@ +using System.Runtime.CompilerServices; +using Text_Grab.Services; + +namespace Text_Grab.Utilities; + +/// +/// Points Text-Grab.Core's at the app's WinRT speech engine. +/// +internal static class TtsEngineAccessInitializer +{ + /// + /// Same reasoning as : a module initializer rather + /// than a call in App.appStartup, so the Tests host is covered too. + /// + [ModuleInitializer] + internal static void Initialize() + => TtsEngineAccess.SetResolver(static () => new WindowsSpeechEngine()); +} diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index b7a336f2..0b99e29a 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -513,6 +513,20 @@ plain Core, after resolving its `private ITtsEngine _engine = new WindowsSpeechE initializer — the app should register the default engine at composition, same shape as `SettingsAccess`. Add `TtsSpeakWordLimit`, `TtsVoiceName`, `TtsSpeakingRate`. +**6b as executed.** Both files moved unsplit, keeping their names. The field initializer became +`Text-Grab.Core/Services/TtsEngineAccess.cs` — the fourth delegate-resolver, after +`SettingsAccess`, `UiThreadAccess` and `InputLanguageAccess`. It holds a `Func` +factory rather than a stored instance, and `TtsService`'s constructor calls +`TtsEngineAccess.CreateDefault()` as its first statement, so the engine is still built at the same +moment it always was — when a `TtsService` is constructed, not lazily on first `Speak`. The app +registers `static () => new WindowsSpeechEngine()` from `Text-Grab/Utilities/ +TtsEngineAccessInitializer.cs`, a `[ModuleInitializer]` covering the Tests host the same way +`SettingsAccessInitializer` does. An unregistered resolver throws `InvalidOperationException`, +matching `SettingsAccess` — in production the module initializer always covers it, so this is +unreachable outside a Core-only host with no fake installed. `WindowsSpeechEngine`'s two settings +reads (`TtsVoiceName`, `TtsSpeakingRate`) moved from `Properties.Settings.Default` to +`SettingsAccess.Current`; `TtsService`'s `TtsSpeakWordLimit` read did the same. + **6c — `AudioTranscriptionUtilities.cs` → Core.Windows, wholesale.** 1115 lines, fully headless (NAudio + Whisper.net, zero WPF, zero WinRT), with exactly **one** settings touchpoint: `AudioTranscriptionModel`. Move `NAudio`, `Whisper.net`, `Whisper.net.Runtime` to Core.Windows. From cd9a7c192364d64d1e144948c9931031754c6e8a Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:42:53 -0500 Subject: [PATCH 39/54] Move AudioTranscriptionUtilities into Text-Grab.Core.Windows wholesale Batch 6c of the Core split. 1115 lines, fully headless NAudio + Whisper.net (local Whisper transcription, live capture, VAD-gated streaming) with zero WPF/WinRT dependencies, moved unsplit to Core.Windows keeping its name and namespace. Its one settings touchpoint - CurrentModelChoice reading AudioTranscriptionModel - switched from AppUtilities.TextGrabSettings to SettingsAccess.Current. AudioTranscriptionModel joined ITextGrabSettings; it already existed in Settings.settings, so no .settings edit was needed. Verified this is the file's only settings read rather than trusting the plan's claim. Unlike the ZXing/CliWrap/Magick.NET precedent - where the app keeps a package because app code still calls those types directly - a repo-wide grep found no remaining app-side use of any NAudio or Whisper.net type, so NAudio, Whisper.net and Whisper.net.Runtime moved to Text-Grab.Core.Windows.csproj outright instead of staying duplicated in Text-Grab.csproj. The two consumers, EditTextWindow.xaml.cs and OpenMediaWindow.xaml.cs, only call AudioTranscriptionUtilities/ LiveAudioTranscriber members and needed no changes. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in the moved file. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, conserved. Co-Authored-By: Claude Opus 5 --- Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj | 9 +++++++++ .../Utilities/AudioTranscriptionUtilities.cs | 3 ++- Text-Grab.Core/Interfaces/ITextGrabSettings.cs | 3 +++ Text-Grab/Text-Grab.csproj | 9 --------- docs/Core-Split-Plan.md | 12 ++++++++++++ 5 files changed, 26 insertions(+), 10 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/AudioTranscriptionUtilities.cs (99%) diff --git a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj index cd5ec575..36b29636 100644 --- a/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj +++ b/Text-Grab.Core.Windows/Text-Grab.Core.Windows.csproj @@ -56,6 +56,15 @@ + + + +
diff --git a/Text-Grab/Utilities/AudioTranscriptionUtilities.cs b/Text-Grab.Core.Windows/Utilities/AudioTranscriptionUtilities.cs similarity index 99% rename from Text-Grab/Utilities/AudioTranscriptionUtilities.cs rename to Text-Grab.Core.Windows/Utilities/AudioTranscriptionUtilities.cs index 676f20f6..eed1ffdb 100644 --- a/Text-Grab/Utilities/AudioTranscriptionUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/AudioTranscriptionUtilities.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Text_Grab.Services; using Whisper.net; using Whisper.net.Ggml; @@ -178,7 +179,7 @@ public static class AudioTranscriptionUtilities "Text-Grab", "WhisperModels"); /// The transcription model currently selected in settings (defaults to multilingual base). - public static WhisperModelChoice CurrentModelChoice => WhisperModelInfo.Parse(AppUtilities.TextGrabSettings.AudioTranscriptionModel); + public static WhisperModelChoice CurrentModelChoice => WhisperModelInfo.Parse(SettingsAccess.Current.AudioTranscriptionModel); private static string ModelPathFor(WhisperModelChoice choice) { diff --git a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs index 5a9f1bed..5bf87b72 100644 --- a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs +++ b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs @@ -71,6 +71,9 @@ public interface ITextGrabSettings /// Speaking rate passed to the TTS engine; only values in [0.5, 6.0] are applied. double TtsSpeakingRate { get; set; } + /// Which local Whisper model to use for on-device audio transcription. + string AudioTranscriptionModel { get; set; } + /// Persist pending changes. Backed by ApplicationSettingsBase.Save(). void Save(); } diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index 0438fe4b..4581d7c2 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -100,15 +100,6 @@ - - - - diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 0b99e29a..a0720059 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -534,6 +534,18 @@ reads (`TtsVoiceName`, `TtsSpeakingRate`) moved from `Properties.Settings.Defaul views, not in this file.) The cleanest single file in the whole reorganization — use it as the anchor that proves Core.Windows can host NAudio/Whisper. +**6c as executed.** Moved unsplit, keeping its name and its `Text_Grab.Utilities` namespace. The +one settings touchpoint (`CurrentModelChoice`, reading `AudioTranscriptionModel`) switched from +`AppUtilities.TextGrabSettings` to `SettingsAccess.Current`; `AudioTranscriptionModel` joined +`ITextGrabSettings` and already existed in `Settings.settings`, so no `.settings` edit was needed. +A repo-wide grep confirmed no other app file uses any NAudio or Whisper.net type directly (unlike +the ZXing/CliWrap/Magick.NET precedent, where the app kept the package because app code still +calls those types), so `NAudio`, `Whisper.net` and `Whisper.net.Runtime` moved to +`Text-Grab.Core.Windows.csproj` outright rather than being duplicated in `Text-Grab.csproj`. The +two consumers (`EditTextWindow.xaml.cs`, `OpenMediaWindow.xaml.cs`) only call +`AudioTranscriptionUtilities`/`LiveAudioTranscriber` members, never NAudio/Whisper.net types +directly, so nothing there needed a change. + **6d — `WebSearchUrlModel` split**, exactly `PatternItem`/`PatternItemCatalog`-shaped: pure record → Core, static accessors stay in the app. No interface changes needed. From baa98f09d589b702228666e5a7f0d9ce0c9790b5 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:46:23 -0500 Subject: [PATCH 40/54] Split WebSearchUrlModel into a pure record and an app-side catalog Batch 6d of the Core split, the PatternItem/PatternItemCatalog shape from e677b54. The settings coupling turned out to be larger than "static accessors": it was on the instance members DefaultSearcher/WebSearchers (and their private backing fields), read through Singleton.Instance at every call site. The three static helpers - GetWebSearchUrls, SaveWebSearchUrls, GetDefaultWebSearchUrls - had zero external call sites; they existed only to back those instance properties. So the whole settings-touching unit moved together into a new app-side Text-Grab/Models/WebSearchUrlCatalog.cs, unchanged apart from the name. Text-Grab.Core/Models/WebSearchUrlModel.cs kept only Name, Url and ToString(). Call-site census: 12 references use WebSearchUrlModel purely as a data type (List, foreach, pattern matches, construction) and needed no edit, since the namespace didn't change. 6 references were Singleton.Instance.{DefaultSearcher,WebSearchers} across GeneralSettings.xaml.cs, PostGrabActionManager.cs and EditTextWindow.xaml.cs, updated to Singleton. The data half's majority is why the original name stayed with it. No ITextGrabSettings changes - the settings-touching code stayed entirely on the app side of the seam. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in either half. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, conserved. Co-Authored-By: Claude Opus 5 --- Text-Grab.Core/Models/WebSearchUrlModel.cs | 17 +++++++++++++++++ ...SearchUrlModel.cs => WebSearchUrlCatalog.cs} | 16 ++++++++++------ Text-Grab/Pages/GeneralSettings.xaml.cs | 6 +++--- Text-Grab/Utilities/PostGrabActionManager.cs | 2 +- Text-Grab/Views/EditTextWindow.xaml.cs | 4 ++-- docs/Core-Split-Plan.md | 15 +++++++++++++++ 6 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 Text-Grab.Core/Models/WebSearchUrlModel.cs rename Text-Grab/Models/{WebSearchUrlModel.cs => WebSearchUrlCatalog.cs} (80%) diff --git a/Text-Grab.Core/Models/WebSearchUrlModel.cs b/Text-Grab.Core/Models/WebSearchUrlModel.cs new file mode 100644 index 00000000..05fb379d --- /dev/null +++ b/Text-Grab.Core/Models/WebSearchUrlModel.cs @@ -0,0 +1,17 @@ +namespace Text_Grab.Models; + +/// +/// A single named web-search endpoint (e.g. "Google" -> "https://www.google.com/search?q="). +/// +/// Settings-backed catalog loading/saving and default-searcher tracking depend on +/// AppUtilities.TextGrabSettings/TextGrabSettingsService, which only exist in the +/// app, so they live in the app-side instead - +/// the same split shape as PatternItem/PatternItemCatalog in e677b54. +/// +public record WebSearchUrlModel +{ + public string Name { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + + public override string ToString() => Name; +} diff --git a/Text-Grab/Models/WebSearchUrlModel.cs b/Text-Grab/Models/WebSearchUrlCatalog.cs similarity index 80% rename from Text-Grab/Models/WebSearchUrlModel.cs rename to Text-Grab/Models/WebSearchUrlCatalog.cs index e70caaa8..04e52dda 100644 --- a/Text-Grab/Models/WebSearchUrlModel.cs +++ b/Text-Grab/Models/WebSearchUrlCatalog.cs @@ -4,11 +4,17 @@ namespace Text_Grab.Models; -public record WebSearchUrlModel +/// +/// Settings-backed catalog of web-search endpoints, plus which +/// one is the default. Split out from because it depends on +/// /, +/// which only exist in the app. Accessed through +/// Singleton<WebSearchUrlCatalog>.Instance so the cached list and default selection +/// persist across the call sites within a session, exactly as they did on the old +/// WebSearchUrlModel singleton instance. +/// +public class WebSearchUrlCatalog { - public string Name { get; set; } = string.Empty; - public string Url { get; set; } = string.Empty; - private WebSearchUrlModel? defaultSearcher; public WebSearchUrlModel DefaultSearcher @@ -25,8 +31,6 @@ public WebSearchUrlModel DefaultSearcher } } - public override string ToString() => Name; - private List webSearchers = []; public List WebSearchers diff --git a/Text-Grab/Pages/GeneralSettings.xaml.cs b/Text-Grab/Pages/GeneralSettings.xaml.cs index 2a721e74..d1d89f48 100644 --- a/Text-Grab/Pages/GeneralSettings.xaml.cs +++ b/Text-Grab/Pages/GeneralSettings.xaml.cs @@ -135,13 +135,13 @@ private async void Page_Loaded(object sender, RoutedEventArgs e) StartupOnLoginCheckBox.IsChecked = DefaultSettings.StartupOnLogin; } - List searcherSettings = Singleton.Instance.WebSearchers; + List searcherSettings = Singleton.Instance.WebSearchers; WebSearchersComboBox.Items.Clear(); foreach (WebSearchUrlModel searcher in searcherSettings) WebSearchersComboBox.Items.Add(searcher); - WebSearchersComboBox.SelectedItem = Singleton.Instance.DefaultSearcher; + WebSearchersComboBox.SelectedItem = Singleton.Instance.DefaultSearcher; ShowToastCheckBox.IsChecked = DefaultSettings.ShowToast; @@ -490,7 +490,7 @@ private void WebSearchersComboBox_SelectionChanged(object sender, SelectionChang || comboBox.SelectedItem is not WebSearchUrlModel newDefault) return; - Singleton.Instance.DefaultSearcher = newDefault; + Singleton.Instance.DefaultSearcher = newDefault; } private async void AddToContextMenuCheckBox_Checked(object sender, RoutedEventArgs e) diff --git a/Text-Grab/Utilities/PostGrabActionManager.cs b/Text-Grab/Utilities/PostGrabActionManager.cs index bbf6451f..45cf09a9 100644 --- a/Text-Grab/Utilities/PostGrabActionManager.cs +++ b/Text-Grab/Utilities/PostGrabActionManager.cs @@ -209,7 +209,7 @@ public static async Task ExecutePostGrabAction(ButtonInfo action, PostGr case "WebSearch_Click": string searchStringUrlSafe = WebUtility.UrlEncode(text); - WebSearchUrlModel searcher = Singleton.Instance.DefaultSearcher; + WebSearchUrlModel searcher = Singleton.Instance.DefaultSearcher; Uri searchUri = new($"{searcher.Url}{searchStringUrlSafe}"); _ = await Windows.System.Launcher.LaunchUriAsync(searchUri); // Don't modify the text for web search diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index d4bba955..3bd57e34 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -3872,7 +3872,7 @@ private async void DefaultWebSearchExecuted(object sender, ExecutedRoutedEventAr string possibleSearch = PassedTextControl.SelectedText; string searchStringUrlSafe = WebUtility.UrlEncode(possibleSearch); - WebSearchUrlModel searcher = Singleton.Instance.DefaultSearcher; + WebSearchUrlModel searcher = Singleton.Instance.DefaultSearcher; Uri searchUri = new($"{searcher.Url}{searchStringUrlSafe}"); _ = await Windows.System.Launcher.LaunchUriAsync(searchUri); @@ -5509,7 +5509,7 @@ private void SetupRoutedCommands() _ = newWindowWithSelectionCommand.InputGestures.Add(new KeyGesture(Key.N, ModifierKeys.Control)); _ = CommandBindings.Add(new CommandBinding(newWindowWithSelectionCommand, NewWindowWithText_Clicked)); - List searchers = Singleton.Instance.WebSearchers; + List searchers = Singleton.Instance.WebSearchers; foreach (WebSearchUrlModel searcher in searchers) { diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index a0720059..a894bcbd 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -549,6 +549,21 @@ directly, so nothing there needed a change. **6d — `WebSearchUrlModel` split**, exactly `PatternItem`/`PatternItemCatalog`-shaped: pure record → Core, static accessors stay in the app. No interface changes needed. +**6d as executed.** The impure half turned out to be larger than "static accessors": the settings +coupling was on *instance* members (`DefaultSearcher`, `WebSearchers` and their private backing +fields), used through `Singleton.Instance` at every call site, with the three +static helpers (`GetWebSearchUrls`, `SaveWebSearchUrls`, `GetDefaultWebSearchUrls`) only ever +called internally to back those properties - zero external call sites of their own. So the whole +settings-touching unit, instance members and statics together, moved into a new app-side +`Text-Grab/Models/WebSearchUrlCatalog.cs`, keeping every member and its behaviour unchanged. +`WebSearchUrlModel` in Core kept only `Name`, `Url` and `ToString()`. Call-site census: 12 +references use `WebSearchUrlModel` purely as a data type (`List`, `foreach`, +pattern matches, construction) and needed no edit since the namespace didn't change; 6 references +were `Singleton.Instance.{DefaultSearcher,WebSearchers}` across +`GeneralSettings.xaml.cs`, `PostGrabActionManager.cs` and `EditTextWindow.xaml.cs`, updated to +`Singleton`. The data half's majority confirms the original name stayed with +it, matching the `PatternItem`/`PatternItemCatalog` precedent. + **6e — `HistoryService.cs`. Opus owns this; it is a second `OcrUtilities`.** A genuinely headless JSON pipeline (`LoadHistoryAsync`, `LoadHistoryWithRecovery`, `WriteHistoryFiles`, the `Normalize*` methods, `HistoryLanguageKindJsonConverter`) is interleaved with WPF menu building, From 3952d9287a00b28b8ce4d57d4cc8e03b87967951 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sat, 29 Aug 2026 23:53:15 -0500 Subject: [PATCH 41/54] Deferred-ledger sweep: move FileUtilities/ContextMenuUtilities, prune stale rows Re-read every section 7 row against the current tree. Three rows named blockers that had since landed: - Utilities/FileUtilities.cs (blocked on AutomationProfile.Current, which landed in Core in 6a). - Utilities/ContextMenuUtilities.cs (blocked on AutomationProfile.Current, FileUtilities.GetExePath(), and the IoUtilities split - all resolved). - Utilities/FileAssociationUtilities.cs (blocked on FileUtilities.GetExePath()). ContextMenuUtilities.cs moved unsplit to Core.Windows. FileUtilities.cs needed one split: GetOpenDocumentFilter() also calls GrabFrameFileUtilities (.GrabFrameFileExtension, .GetGrabFrameFileFilter()), which stays app-side - blocked on HistoryInfo per its own section 7 row, untouched here since Services/HistoryService.cs is explicitly out of scope for this sweep. Everything else in FileUtilities (12 other members, a dozen-plus call sites across the app) moved to Core.Windows keeping the name. GetOpenDocumentFilter() alone (3 call sites: App.xaml.cs, EditTextWindow.xaml.cs, one test) moved into a new app-side Text-Grab/Utilities/OpenDocumentFilterUtilities.cs, calling back into two of FileUtilities's helpers (GetVisualDocumentFilterPattern, GetExtensionsFilterPattern) widened from private to internal for exactly that caller. AppUtilities.IsPackaged() calls in FileUtilities became PackageIdentity.IsPackaged() - the established 4e substitution, since AppUtilities lives in the app and Core.Windows cannot see it. FileAssociationUtilities.cs did not move: its GrabFrameExtensionKeyPath constant references GrabFrameFileUtilities.GrabFrameFileExtension directly - the same HistoryInfo blocker one level removed, not previously recorded. Its section 7 row was rewritten with the real blocker rather than forcing the move. Utilities/TesseractHelper.cs's row was stale - it moved to Core.Windows in batch 4b and the row was simply never removed - deleted. Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/ GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/ .CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource lookups in either moved file. FileUtilities.GetExePath() already used Environment.ProcessPath rather than assembly-location reflection, so it needed no change for this. Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean. Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510 total, conserved. Co-Authored-By: Claude Opus 5 --- Tests/FilesIoTests.cs | 2 +- .../Utilities/ContextMenuUtilities.cs | 0 .../Utilities/FileUtilities.cs | 49 ++++++------------- Text-Grab/App.xaml.cs | 2 +- .../Utilities/OpenDocumentFilterUtilities.cs | 42 ++++++++++++++++ Text-Grab/Views/EditTextWindow.xaml.cs | 2 +- docs/Core-Split-Plan.md | 26 ++++++++-- 7 files changed, 83 insertions(+), 40 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/ContextMenuUtilities.cs (100%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/FileUtilities.cs (88%) create mode 100644 Text-Grab/Utilities/OpenDocumentFilterUtilities.cs diff --git a/Tests/FilesIoTests.cs b/Tests/FilesIoTests.cs index 6560b7ca..0e54a134 100644 --- a/Tests/FilesIoTests.cs +++ b/Tests/FilesIoTests.cs @@ -142,7 +142,7 @@ public void GetVisualDocumentFilter_IncludesPdfSupport() [Fact] public void GetOpenDocumentFilter_IncludesVisualAndTextOptions() { - string filter = FileUtilities.GetOpenDocumentFilter(); + string filter = OpenDocumentFilterUtilities.GetOpenDocumentFilter(); Assert.Contains("Supported documents|", filter); Assert.Contains("Image and PDF files|", filter); diff --git a/Text-Grab/Utilities/ContextMenuUtilities.cs b/Text-Grab.Core.Windows/Utilities/ContextMenuUtilities.cs similarity index 100% rename from Text-Grab/Utilities/ContextMenuUtilities.cs rename to Text-Grab.Core.Windows/Utilities/ContextMenuUtilities.cs diff --git a/Text-Grab/Utilities/FileUtilities.cs b/Text-Grab.Core.Windows/Utilities/FileUtilities.cs similarity index 88% rename from Text-Grab/Utilities/FileUtilities.cs rename to Text-Grab.Core.Windows/Utilities/FileUtilities.cs index f9958b9c..e916a53d 100644 --- a/Text-Grab/Utilities/FileUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/FileUtilities.cs @@ -18,7 +18,7 @@ public class FileUtilities if (AutomationProfile.Current is not null) return GetImageFileUnpackaged(fileName, storageKind); - if (AppUtilities.IsPackaged() && AutomationProfile.Current is null) + if (PackageIdentity.IsPackaged() && AutomationProfile.Current is null) return GetImageFilePackaged(fileName, storageKind); return GetImageFileUnpackaged(fileName, storageKind); @@ -53,31 +53,11 @@ public static string GetVisualDocumentFilter() }); } - public static string GetOpenDocumentFilter() - { - string spreadsheetExtensions = GetExtensionsFilterPattern(IoUtilities.SpreadsheetExtensions); - string markdownExtensions = GetExtensionsFilterPattern(IoUtilities.MarkdownExtensions); - string grabFrameExtension = $"*{GrabFrameFileUtilities.GrabFrameFileExtension}"; - string supportedExtensions = string.Join(";", new[] - { - GetVisualDocumentFilterPattern(), - grabFrameExtension, - spreadsheetExtensions, - markdownExtensions, - "*.txt" - }.Where(pattern => !string.IsNullOrWhiteSpace(pattern))); - - return string.Join("|", new[] - { - $"Supported documents|{supportedExtensions}", - GetVisualDocumentFilter(), - GrabFrameFileUtilities.GetGrabFrameFileFilter(), - $"Spreadsheet documents|{spreadsheetExtensions}", - $"Markdown documents|{markdownExtensions}", - "Text documents (*.txt)|*.txt", - "All files (*.*)|*.*" - }); - } + // GetOpenDocumentFilter() is NOT here: it also needs GrabFrameFileUtilities. + // GrabFrameFileUtilities stays app-side (blocked on HistoryInfo - see its section 7 row), so + // that filter builder lives in the app as OpenDocumentFilterUtilities.GetOpenDocumentFilter(), + // which calls back into GetVisualDocumentFilterPattern() and GetExtensionsFilterPattern() + // below (internal, for exactly that caller). public static string GetPathToLocalFile(string imageRelativePath) { @@ -94,7 +74,7 @@ public static async Task GetPathToHistory() if (AutomationProfile.Current is AutomationProfile profile) return profile.HistoryDirectory; - if (AppUtilities.IsPackaged()) + if (PackageIdentity.IsPackaged()) { StorageFolder historyFolder = await GetStorageFolderPackaged("", FileStorageKind.WithHistory); return historyFolder.Path; @@ -108,7 +88,7 @@ public static Task GetTextFileAsync(string fileName, FileStorageKind sto if (AutomationProfile.Current is not null) return GetTextFileUnpackaged(fileName, storageKind); - if (AppUtilities.IsPackaged()) + if (PackageIdentity.IsPackaged()) return GetTextFilePackaged(fileName, storageKind); return GetTextFileUnpackaged(fileName, storageKind); @@ -119,7 +99,7 @@ public static Task SaveImageFile(Bitmap image, string filename, FileStorag if (AutomationProfile.Current is not null) return SaveImageFileUnpackaged(image, filename, storageKind); - if (AppUtilities.IsPackaged()) + if (PackageIdentity.IsPackaged()) return SaveImagePackaged(image, filename, storageKind); return SaveImageFileUnpackaged(image, filename, storageKind); @@ -130,7 +110,7 @@ public static Task SaveTextFile(string textContent, string filename, FileS if (AutomationProfile.Current is not null) return SaveTextFileUnpackaged(textContent, filename, storageKind); - if (AppUtilities.IsPackaged()) + if (PackageIdentity.IsPackaged()) return SaveTextFilePackaged(textContent, filename, storageKind); return SaveTextFileUnpackaged(textContent, filename, storageKind); @@ -156,12 +136,15 @@ private static string GetImageExtensionsFilterPattern() return imageExtensions; } - private static string GetExtensionsFilterPattern(IEnumerable extensions) + // internal rather than private: OpenDocumentFilterUtilities (app-side, split out because it + // also needs GrabFrameFileUtilities) calls this to build its own filter pattern. + internal static string GetExtensionsFilterPattern(IEnumerable extensions) { return string.Join(";", extensions.Select(extension => $"*{extension}")); } - private static string GetVisualDocumentFilterPattern() + // internal for the same reason as GetExtensionsFilterPattern above. + internal static string GetVisualDocumentFilterPattern() { return string.Join(";", new[] { @@ -369,7 +352,7 @@ private static async Task SaveTextFileUnpackaged(string textContent, strin public static async void TryDeleteHistoryDirectory() { FileStorageKind historyFolderKind = FileStorageKind.WithHistory; - if (AppUtilities.IsPackaged() && AutomationProfile.Current is null) + if (PackageIdentity.IsPackaged() && AutomationProfile.Current is null) { StorageFolder historyFolder = await GetStorageFolderPackaged("", historyFolderKind); diff --git a/Text-Grab/App.xaml.cs b/Text-Grab/App.xaml.cs index 9af38c47..e877f490 100644 --- a/Text-Grab/App.xaml.cs +++ b/Text-Grab/App.xaml.cs @@ -84,7 +84,7 @@ public static async Task OpenFileWithPickerAsync(bool isQuiet = false) { OpenFileDialog openFileDialog = new() { - Filter = FileUtilities.GetOpenDocumentFilter(), + Filter = OpenDocumentFilterUtilities.GetOpenDocumentFilter(), Title = "Open File", CheckFileExists = true, InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) diff --git a/Text-Grab/Utilities/OpenDocumentFilterUtilities.cs b/Text-Grab/Utilities/OpenDocumentFilterUtilities.cs new file mode 100644 index 00000000..6b2ec2a6 --- /dev/null +++ b/Text-Grab/Utilities/OpenDocumentFilterUtilities.cs @@ -0,0 +1,42 @@ +using System.Linq; + +namespace Text_Grab.Utilities; + +/// +/// Impure half of the "Open document" filter, split out of in the +/// deferred-ledger sweep because it also needs , which stays +/// app-side (blocked on HistoryInfo - see its section 7 row). Everything else that used to +/// live alongside it (, +/// ) moved to Text-Grab.Core.Windows under the original +/// FileUtilities name; this calls back into its two now-internal helpers, +/// and +/// , to build the same string it always did. +/// +public static class OpenDocumentFilterUtilities +{ + public static string GetOpenDocumentFilter() + { + string spreadsheetExtensions = FileUtilities.GetExtensionsFilterPattern(IoUtilities.SpreadsheetExtensions); + string markdownExtensions = FileUtilities.GetExtensionsFilterPattern(IoUtilities.MarkdownExtensions); + string grabFrameExtension = $"*{GrabFrameFileUtilities.GrabFrameFileExtension}"; + string supportedExtensions = string.Join(";", new[] + { + FileUtilities.GetVisualDocumentFilterPattern(), + grabFrameExtension, + spreadsheetExtensions, + markdownExtensions, + "*.txt" + }.Where(pattern => !string.IsNullOrWhiteSpace(pattern))); + + return string.Join("|", new[] + { + $"Supported documents|{supportedExtensions}", + FileUtilities.GetVisualDocumentFilter(), + GrabFrameFileUtilities.GetGrabFrameFileFilter(), + $"Spreadsheet documents|{spreadsheetExtensions}", + $"Markdown documents|{markdownExtensions}", + "Text documents (*.txt)|*.txt", + "All files (*.*)|*.*" + }); + } +} diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index 3bd57e34..b6126e06 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -4426,7 +4426,7 @@ private void OpenFileMenuItem_Click(object sender, RoutedEventArgs e) { // Set filter for file extension and default file extension DefaultExt = ".txt", - Filter = FileUtilities.GetOpenDocumentFilter(), + Filter = OpenDocumentFilterUtilities.GetOpenDocumentFilter(), DefaultDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) }; diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index a894bcbd..698ddb5a 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -564,6 +564,27 @@ were `Singleton.Instance.{DefaultSearcher,WebSearchers}` acro `Singleton`. The data half's majority confirms the original name stayed with it, matching the `PatternItem`/`PatternItemCatalog` precedent. +**Deferred-ledger sweep, run alongside 6b-6d.** Three §7 rows named blockers that had since +landed: `Utilities/FileUtilities.cs` (blocked on `AutomationProfile.Current`, resolved in 6a), +`Utilities/FileAssociationUtilities.cs` (blocked on `FileUtilities.GetExePath()`), and +`Utilities/ContextMenuUtilities.cs` (blocked on `AutomationProfile.Current`, +`FileUtilities.GetExePath()`, and the `IoUtilities` split, all resolved). Two moved cleanly: +`ContextMenuUtilities.cs` moved unsplit. `FileUtilities.cs` needed one split: its +`GetOpenDocumentFilter()` also calls `GrabFrameFileUtilities` (`.GrabFrameFileExtension`, +`.GetGrabFrameFileFilter()`), which stays app-side - blocked on `HistoryInfo`, per its own §7 row, +untouched here since `Services/HistoryService.cs` is out of scope for this sweep. Everything else +in `FileUtilities` (12 other members, a dozen-plus call sites across the app) moved to +Core.Windows keeping the name; `GetOpenDocumentFilter()` alone (3 call sites: `App.xaml.cs`, +`EditTextWindow.xaml.cs`, one test) moved into a new app-side `OpenDocumentFilterUtilities.cs`, +calling back into two of `FileUtilities`'s helpers (`GetVisualDocumentFilterPattern`, +`GetExtensionsFilterPattern`) widened from `private` to `internal` for exactly that caller. +`AppUtilities.IsPackaged()` calls in `FileUtilities` became `PackageIdentity.IsPackaged()` (the +established 4e substitution - `AppUtilities.IsPackaged()` is a one-line forwarder to it). +`FileAssociationUtilities.cs` did **not** move: its `GrabFrameExtensionKeyPath` constant +references `GrabFrameFileUtilities.GrabFrameFileExtension` directly, the same `HistoryInfo` +blocker one level removed - left in place with its §7 row rewritten. `Utilities/TesseractHelper.cs` +was a stale §7 row - it moved in batch 4b and was simply never removed - deleted. + **6e — `HistoryService.cs`. Opus owns this; it is a second `OcrUtilities`.** A genuinely headless JSON pipeline (`LoadHistoryAsync`, `LoadHistoryWithRecovery`, `WriteHistoryFiles`, the `Normalize*` methods, `HistoryLanguageKindJsonConverter`) is interleaved with WPF menu building, @@ -760,10 +781,7 @@ Every row below was verified by reading the file, not inferred. | File | Blocker (specific) | Unblocked by | |---|---|---| | `Utilities/OcrSourceUtilities.cs` | Post-4c remainder. `LoadBitmapFromFile` builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite, and it takes `OcrAbsoluteFilePathAsync` and `OcrFile` with it. Engine dispatch additionally needs `WindowsAiUtilities` (5a) and `LanguageUtilities` (4e); the rest is `Window`/`BitmapSource` capture and stays | a GDI+/WIC rewrite, then 5a + 4e | -| `Utilities/TesseractHelper.cs` | `TempImagePath()` calls `AutomationProfile.GetTemporaryDirectory()`. The settings write-back is **not** a blocker — `ITextGrabSettings.Save()` already covers it | 6a | -| `Utilities/ContextMenuUtilities.cs` | `AutomationProfile.Current`, `FileUtilities.GetExePath()`; `IoUtilities` mixes pure extension lists with WinForms/Wpf.Ui `MessageBox` calls | 1a (IoUtilities split), 5, 6a | -| `Utilities/FileAssociationUtilities.cs` | `FileUtilities.GetExePath()` | Wave 5 | -| `Utilities/FileUtilities.cs` | `AutomationProfile.Current` in 6 methods | 6a | +| `Utilities/FileAssociationUtilities.cs` | `GrabFrameExtensionKeyPath` is a `const` field initializer referencing `GrabFrameFileUtilities.GrabFrameFileExtension`, which requires the type at compile time. `GrabFrameFileUtilities` stays app-side (see its own row below), so this file does too - the same root blocker, one level removed | `GrabFrameFileUtilities` moving (`HistoryInfo`) | | `Utilities/GrabFrameFileUtilities.cs` | Public signature bound to `HistoryInfo`, which needs B2 (`Rect PositionRect`), 2a (five enums) and 4e. A façade was considered and rejected as larger than the file it wraps | 2d + 4e, or `HistoryInfo` moving | | `Services/SettingsService.cs` | Clones `ButtonInfo` and `ShortcutKeySet` field-by-field (both never-move); `Windows.Storage.ApplicationDataContainer` caps it at Core.Windows regardless | needs a `ButtonInfo` redesign — likely never | | `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so the remaining blocker is a plain split | a split | From a8591aa48f2da3d081a7e1a339d3ff2ad53c2eb5 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 09:58:40 -0500 Subject: [PATCH 42/54] Move HistoryInfo to Core.Windows, retiring its System.Windows.Rect HistoryInfo.PositionRect was the last System.Windows.Rect standing between the history model and Core.Windows, and it is what blocked 6e (the HistoryService split) and the GrabFrameFileUtilities row in section 7. PositionRect was never a stored field - it is a projection over the persisted RectAsString - so B2's currency change costs nothing on disk. It is now a System.Drawing.RectangleF with hand-rolled parse/format helpers that keep the "x,y,width,height" text Rect.ToString() used to write, plus the literal "Empty". Writing is invariant-culture now, and reading tolerates the ';' separator and comma decimals Rect.ToString() emitted under cultures whose decimal separator is ',' - strings the old invariant-only Rect.Parse threw on rather than read. Call sites convert at the edge through ShapeExtensions (AsRect/AsRectangleF), the established B2 pattern. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Tests/GrabFrameFileTests.cs | 6 +- .../Models/HistoryInfo.cs | 66 +++++++++++++++---- Text-Grab/Utilities/OcrSourceUtilities.cs | 8 +-- Text-Grab/Views/EditTextWindow.xaml.cs | 4 +- .../Views/FullscreenGrab.SelectionStyles.cs | 4 +- Text-Grab/Views/GrabFrame.xaml.cs | 8 +-- 6 files changed, 67 insertions(+), 29 deletions(-) rename {Text-Grab => Text-Grab.Core.Windows}/Models/HistoryInfo.cs (70%) diff --git a/Tests/GrabFrameFileTests.cs b/Tests/GrabFrameFileTests.cs index cf0bfa81..fc969a14 100644 --- a/Tests/GrabFrameFileTests.cs +++ b/Tests/GrabFrameFileTests.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using System.IO; using System.IO.Compression; using System.Text.Json; @@ -40,7 +40,7 @@ public async Task SaveAndLoad_RoundTripsMetadataWordBordersAndImage() IsTable = true, LanguageTag = "en-US", LanguageKind = LanguageKind.Global, - PositionRect = new Rect(100, 120, 400, 300), + PositionRect = new RectangleF(100, 120, 400, 300), WordBorderInfoJson = JsonSerializer.Serialize(wordBorders), ImageContent = new Bitmap(64, 48), }; @@ -60,7 +60,7 @@ public async Task SaveAndLoad_RoundTripsMetadataWordBordersAndImage() Assert.True(loaded.IsTable); Assert.Equal("en-US", loaded.LanguageTag); Assert.Equal(LanguageKind.Global, loaded.LanguageKind); - Assert.Equal(new Rect(100, 120, 400, 300), loaded.PositionRect); + Assert.Equal(new RectangleF(100, 120, 400, 300), loaded.PositionRect); Assert.NotNull(loaded.ImageContent); Assert.Equal(64, loaded.ImageContent!.Width); diff --git a/Text-Grab/Models/HistoryInfo.cs b/Text-Grab.Core.Windows/Models/HistoryInfo.cs similarity index 70% rename from Text-Grab/Models/HistoryInfo.cs rename to Text-Grab.Core.Windows/Models/HistoryInfo.cs index d541eed8..61bfc3d9 100644 --- a/Text-Grab/Models/HistoryInfo.cs +++ b/Text-Grab.Core.Windows/Models/HistoryInfo.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Globalization; using System.Text.Json.Serialization; -using System.Windows; using Text_Grab.Interfaces; using Text_Grab.Utilities; using Windows.Globalization; @@ -11,6 +11,8 @@ namespace Text_Grab.Models; public class HistoryInfo : IEquatable { + private static readonly NumberFormatInfo CommaDecimalFormat = new() { NumberDecimalSeparator = "," }; + #region Constructors public HistoryInfo() @@ -91,21 +93,22 @@ public ILanguage OcrLanguage } } + /// + /// A projection over the persisted , not a stored field. + /// + /// + /// The on-disk format is the one System.Windows.Rect wrote before B2 of the Core split + /// moved this model off WPF geometry: "x,y,width,height", or the literal "Empty". + /// It is written with the invariant culture so a history file stays readable on any machine, + /// and read back tolerating the ';' separator and comma decimals that + /// Rect.ToString() produced under cultures whose decimal separator is ',' - + /// strings the old invariant-only Rect.Parse threw on. + /// [JsonIgnore] - public Rect PositionRect + public RectangleF PositionRect { - get - { - if (string.IsNullOrWhiteSpace(RectAsString)) - return Rect.Empty; - - return Rect.Parse(RectAsString); - } - - set - { - RectAsString = value.ToString(); - } + get => ParsePositionRect(RectAsString); + set => RectAsString = FormatPositionRect(value); } public TextGrabMode SourceMode { get; set; } @@ -177,5 +180,40 @@ public override int GetHashCode() return HashCode.Combine(ID); } + private static RectangleF ParsePositionRect(string source) + { + if (string.IsNullOrWhiteSpace(source)) + return RectangleF.Empty; + + string trimmed = source.Trim(); + + if (trimmed.Equals("Empty", StringComparison.OrdinalIgnoreCase)) + return RectangleF.Empty; + + // A ';' separator means the writing culture used ',' as its decimal separator. + bool commaDecimals = trimmed.Contains(';'); + string[] parts = trimmed.Split(commaDecimals ? ';' : ','); + + if (parts.Length != 4) + return RectangleF.Empty; + + IFormatProvider format = commaDecimals ? CommaDecimalFormat : CultureInfo.InvariantCulture; + float[] values = new float[4]; + + for (int i = 0; i < 4; i++) + if (!float.TryParse(parts[i].Trim(), NumberStyles.Float, format, out values[i])) + return RectangleF.Empty; + + return new RectangleF(values[0], values[1], values[2], values[3]); + } + + private static string FormatPositionRect(RectangleF rect) + { + if (rect == RectangleF.Empty) + return string.Empty; + + return string.Create(CultureInfo.InvariantCulture, $"{rect.X},{rect.Y},{rect.Width},{rect.Height}"); + } + #endregion Public Methods } diff --git a/Text-Grab/Utilities/OcrSourceUtilities.cs b/Text-Grab/Utilities/OcrSourceUtilities.cs index d9ed53e0..58ebf887 100644 --- a/Text-Grab/Utilities/OcrSourceUtilities.cs +++ b/Text-Grab/Utilities/OcrSourceUtilities.cs @@ -277,14 +277,14 @@ public static async void GetCopyTextFromPreviousRegion() if (!await CanReplayPreviousFullscreenSelection(lastFsg)) return; - Rect scaledRect = lastFsg.PositionRect.GetScaledUpByFraction(lastFsg.DpiScaleFactor); + Rect scaledRect = lastFsg.PositionRect.GetScaledUpByFraction(lastFsg.DpiScaleFactor).AsRect(); ILanguage language = lastFsg.OcrLanguage ?? LanguageUtilities.GetCurrentInputLanguage(); // Capture the region before showing the loading indicator so the overlay itself // isn't baked into the region's screenshot (issue #662). Bitmap preCapturedBitmap = ImageMethods.GetRegionOfScreenAsBitmap(scaledRect.AsRectangle()); - PreviousGrabWindow previousGrab = new(lastFsg.PositionRect, PreviousGrabIndicator.Loading); + PreviousGrabWindow previousGrab = new(lastFsg.PositionRect.AsRect(), PreviousGrabIndicator.Loading); previousGrab.Show(); try @@ -329,14 +329,14 @@ public static async Task GetTextFromPreviousFullscreenRegion(TextBox? destinatio if (!await CanReplayPreviousFullscreenSelection(lastFsg)) return; - Rect scaledRect = lastFsg.PositionRect.GetScaledUpByFraction(lastFsg.DpiScaleFactor); + Rect scaledRect = lastFsg.PositionRect.GetScaledUpByFraction(lastFsg.DpiScaleFactor).AsRect(); ILanguage language = lastFsg.OcrLanguage ?? LanguageUtilities.GetCurrentInputLanguage(); // Capture the region before showing the loading indicator so the overlay itself // isn't baked into the region's screenshot (issue #662). Bitmap preCapturedBitmap = ImageMethods.GetRegionOfScreenAsBitmap(scaledRect.AsRectangle()); - PreviousGrabWindow previousGrab = new(lastFsg.PositionRect, PreviousGrabIndicator.Loading); + PreviousGrabWindow previousGrab = new(lastFsg.PositionRect.AsRect(), PreviousGrabIndicator.Loading); previousGrab.Show(); try diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index b6126e06..085cf2ce 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -1,4 +1,4 @@ -using Humanizer; +using Humanizer; using System; using System.Collections.Generic; using System.ComponentModel; @@ -194,7 +194,7 @@ public EditTextWindow(HistoryInfo historyInfo) historyId = historyInfo.ID; - if (historyInfo.PositionRect != Rect.Empty) + if (historyInfo.PositionRect != System.Drawing.RectangleF.Empty) { this.Left = historyInfo.PositionRect.X; this.Top = historyInfo.PositionRect.Y; diff --git a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs index c52fcc89..395c1957 100644 --- a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs +++ b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs @@ -1,4 +1,4 @@ -using Dapplo.Windows.User32; +using Dapplo.Windows.User32; using System; using System.Collections.Generic; using System.Linq; @@ -1328,7 +1328,7 @@ private async Task FinishCommitWithTextAsync(FullscreenCaptureResult selec LanguageKind = languageKind, UsedUiAutomation = usedUiAutomation, CaptureDateTime = DateTimeOffset.Now, - PositionRect = GetHistoryPositionRect(selection), + PositionRect = GetHistoryPositionRect(selection).AsRectangleF(), IsTable = TableToggleButton.IsChecked!.Value, TextContent = TextFromOCR ?? string.Empty, ImageContent = historyBitmap, diff --git a/Text-Grab/Views/GrabFrame.xaml.cs b/Text-Grab/Views/GrabFrame.xaml.cs index 9b705f49..11ef299a 100644 --- a/Text-Grab/Views/GrabFrame.xaml.cs +++ b/Text-Grab/Views/GrabFrame.xaml.cs @@ -370,7 +370,7 @@ private async Task LoadContentFromHistory(HistoryInfo history) if (wbInfoList.Count < 1) NotifyIfUiAutomationNeedsLiveSource(currentLanguage); - if (history.PositionRect != Rect.Empty) + if (history.PositionRect != System.Drawing.RectangleF.Empty) { Left = history.PositionRect.Left; Top = history.PositionRect.Top; @@ -507,8 +507,8 @@ private Size GetSavedHistoryContentSize(HistoryInfo history) return new Size(imageContentBitmap.Width, imageContentBitmap.Height); } - Rect positionRect = history.PositionRect; - if (positionRect == Rect.Empty || positionRect.Width <= 0 || positionRect.Height <= 0) + System.Drawing.RectangleF positionRect = history.PositionRect; + if (positionRect == System.Drawing.RectangleF.Empty || positionRect.Width <= 0 || positionRect.Height <= 0) return new Size(0, 0); if (history.SourceMode == TextGrabMode.Fullscreen) @@ -1262,7 +1262,7 @@ public HistoryInfo AsHistoryItem() WordBorderInfoJson = wbInfoJson, WordBorderInfoFileName = wbInfoJson is null ? null : historyItem?.WordBorderInfoFileName, ImageContent = bitmap, - PositionRect = sizePosRect, + PositionRect = sizePosRect.AsRectangleF(), IsTable = TableToggleButton.IsChecked!.Value, ManualTableColumnSeparators = tableEditState.ManualColumnSeparators.Count > 0 ? [.. tableEditState.ManualColumnSeparators] : null, ManualTableRowSeparators = tableEditState.ManualRowSeparators.Count > 0 ? [.. tableEditState.ManualRowSeparators] : null, From 212234a872b5a203c5b6b28c94f53091ff3f5a40 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:07:57 -0500 Subject: [PATCH 43/54] 6e: split the history persistence pipeline out of HistoryService HistoryService was a headless JSON pipeline tangled up with a WPF service. The pipeline half is now Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs - loading and writing the two history files, item-by-item recovery when one is corrupt, the LanguageKind converter and its AsyncLocal rewrite flag, the word-border sidecar files, the two normalization passes, retention, and artifact deletion. All static, no state past the serializer options. HistoryService keeps its name (every call site is Singleton) and keeps what actually held it in the app: the in-memory lists, the two DispatcherTimers that debounce writes and release the idle cache, the cached fullscreen bitmap, the recent-grabs MenuItem building, and the SaveToHistory overloads that take a GrabFrame and an EditTextWindow. Two behaviour-preserving details worth naming: - NormalizeHistoryIds used to call MarkHistoryDirty itself; it now returns a bool so it can be static. Its callers evaluate it and NormalizeHistoryCompatibilityData into locals first - both normalizers mutate, so neither may be short-circuited away by the other, which || would have done. - GetWordBorderInfosAsync stays on the service as a wrapper, because the TouchHistoryCache() it starts with is cache bookkeeping, not file work. PersistWordBorderData reads EnableFileBackedManagedSettings, so that joins ITextGrabSettings. GetMostRecentGrab and GetExcessVisualHistoryItems moved with the pipeline, so HistoryServiceTests calls them on the new type. Also strips BOMs this session's tooling added to five files that never had one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Tests/GrabFrameFileTests.cs | 2 +- Tests/HistoryServiceTests.cs | 6 +- .../Utilities/HistoryFileUtilities.cs | 503 +++++++++++++++++ .../Interfaces/ITextGrabSettings.cs | 3 + Text-Grab/Services/HistoryService.cs | 528 +++--------------- Text-Grab/Views/EditTextWindow.xaml.cs | 2 +- .../Views/FullscreenGrab.SelectionStyles.cs | 2 +- 7 files changed, 576 insertions(+), 470 deletions(-) create mode 100644 Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs diff --git a/Tests/GrabFrameFileTests.cs b/Tests/GrabFrameFileTests.cs index fc969a14..50577017 100644 --- a/Tests/GrabFrameFileTests.cs +++ b/Tests/GrabFrameFileTests.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using System.IO; using System.IO.Compression; using System.Text.Json; diff --git a/Tests/HistoryServiceTests.cs b/Tests/HistoryServiceTests.cs index 36ca2cef..16a6343f 100644 --- a/Tests/HistoryServiceTests.cs +++ b/Tests/HistoryServiceTests.cs @@ -123,7 +123,7 @@ public void ImageHistory_SeparatesPdfDocumentsFromRecentGrabs() Assert.Same(newerPdf, Assert.Single(historyService.GetRecentPdfDocuments())); Assert.Equal(4, newerPdf.SourcePageIndex); Assert.True(historyService.HasAnyRecentGrabs()); - Assert.Same(olderGrab, HistoryService.GetMostRecentGrab([olderGrab, newerPdf])); + Assert.Same(olderGrab, HistoryFileUtilities.GetMostRecentGrab([olderGrab, newerPdf])); } [Fact] @@ -135,7 +135,7 @@ public void GetMostRecentGrab_ReturnsNull_WhenHistoryOnlyContainsPdfs() SourceContentKind = OpenContentKind.PdfDocument, }; - Assert.Null(HistoryService.GetMostRecentGrab([pdf])); + Assert.Null(HistoryFileUtilities.GetMostRecentGrab([pdf])); } [Fact] @@ -158,7 +158,7 @@ public void VisualHistoryRetention_LimitsGrabsAndPdfsIndependently() }); } - List itemsToRemove = HistoryService.GetExcessVisualHistoryItems(historyItems); + List itemsToRemove = HistoryFileUtilities.GetExcessVisualHistoryItems(historyItems); Assert.Equal(4, itemsToRemove.Count); Assert.Contains(itemsToRemove, history => history.ID == "grab-0"); diff --git a/Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs b/Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs new file mode 100644 index 00000000..c0be1a45 --- /dev/null +++ b/Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Text_Grab.Models; +using Text_Grab.Services; + +namespace Text_Grab.Utilities; + +/// +/// The on-disk half of the grab history: reading and writing the two history JSON files, the +/// word-border sidecar files beside them, the normalization passes that keep older files +/// loadable, and the retention rules that decide what gets dropped. +/// +/// The headless half of what used to be HistoryService (batch 6e of the Core split). Everything +/// here is static and owns no state past the serializer options - the in-memory history lists, +/// the DispatcherTimer-driven write debounce and cache-release cycle, the cached fullscreen +/// bitmap, the recent-grabs MenuItem building and the GrabFrame / EditTextWindow construction all +/// stayed behind in Text_Grab.Services.HistoryService, which still owns the state these +/// functions are handed. +/// +/// Retention lives here rather than with the service because the caps and the selection rule are +/// one idea: picks what to drop and +/// and its siblings cap what gets written. +/// +public static class HistoryFileUtilities +{ + #region Fields + + /// How many text-only history entries survive a write. + internal const int MaxHistoryTextOnly = 100; + + /// How many image-backed (non-PDF) history entries survive a write. + internal const int MaxHistoryWithImages = 10; + + /// How many PDF-sourced history entries survive a write. + internal const int MaxHistoryPdfDocuments = 10; + + private const string WordBorderInfoFileSuffix = ".wordborders.json"; + + private static readonly AsyncLocal HistoryLanguageKindFallbackUsed = new(); + + private static readonly JsonSerializerOptions HistoryJsonOptions = new() + { + AllowTrailingCommas = true, + WriteIndented = true, + Converters = + { + new HistoryLanguageKindJsonConverter(), + new JsonStringEnumConverter() + } + }; + + #endregion Fields + + #region Loading and writing + + internal static async Task<(List HistoryItems, bool NeedsRewrite)> LoadHistoryAsync(string fileName) + { + string rawText = await FileUtilities.GetTextFileAsync($"{fileName}.json", FileStorageKind.WithHistory); + + if (string.IsNullOrWhiteSpace(rawText)) + return ([], false); + + try + { + HistoryLanguageKindFallbackUsed.Value = false; + List? tempHistory = JsonSerializer.Deserialize>(rawText, HistoryJsonOptions); + + if (tempHistory is List jsonList && jsonList.Count > 0) + return (tempHistory, HistoryLanguageKindFallbackUsed.Value); + } + catch (JsonException ex) + { + Debug.WriteLine($"Failed to deserialize history file '{fileName}.json' as a list. Attempting item-by-item recovery. {ex}"); + return LoadHistoryWithRecovery(rawText, fileName); + } + finally + { + HistoryLanguageKindFallbackUsed.Value = false; + } + + return ([], false); + } + + internal static (List HistoryItems, bool NeedsRewrite) LoadHistoryBlocking(string fileName) + { + return Task.Run(() => LoadHistoryAsync(fileName)).GetAwaiter().GetResult(); + } + + private static (List HistoryItems, bool NeedsRewrite) LoadHistoryWithRecovery(string rawText, string fileName) + { + try + { + using JsonDocument document = JsonDocument.Parse(rawText); + + if (document.RootElement.ValueKind != JsonValueKind.Array) + return ([], true); + + List recoveredHistory = []; + bool needsRewrite = true; + int index = 0; + + foreach (JsonElement element in document.RootElement.EnumerateArray()) + { + try + { + HistoryLanguageKindFallbackUsed.Value = false; + HistoryInfo? historyItem = element.Deserialize(HistoryJsonOptions); + if (historyItem is not null) + { + recoveredHistory.Add(historyItem); + if (HistoryLanguageKindFallbackUsed.Value) + needsRewrite = true; + } + } + catch (JsonException ex) + { + Debug.WriteLine($"Skipped invalid history item at index {index} from '{fileName}.json'. {ex}"); + } + finally + { + HistoryLanguageKindFallbackUsed.Value = false; + } + + index++; + } + + return (recoveredHistory, needsRewrite); + } + catch (JsonException ex) + { + Debug.WriteLine($"Failed to parse history file '{fileName}.json' during recovery. {ex}"); + return ([], true); + } + } + + internal static void WriteHistoryFiles(List history, string fileName, int maxNumberToSave) + { + string historyAsJson = JsonSerializer + .Serialize(history + .OrderBy(x => x.CaptureDateTime) + .TakeLast(maxNumberToSave), + HistoryJsonOptions); + + try + { + SaveHistoryTextFileBlocking(historyAsJson, $"{fileName}.json"); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to save history json file. {ex.Message}"); + } + } + + #endregion Loading and writing + + #region Normalization + + internal static bool NormalizeHistoryIds(List historyItems) + { + HashSet seenIds = []; + bool updatedAnyIds = false; + + foreach (HistoryInfo historyItem in historyItems) + { + if (!string.IsNullOrWhiteSpace(historyItem.ID) && seenIds.Add(historyItem.ID)) + continue; + + string nextId; + do + { + nextId = Guid.NewGuid().ToString(); + } + while (!seenIds.Add(nextId)); + + historyItem.ID = nextId; + updatedAnyIds = true; + } + + return updatedAnyIds; + } + + internal static bool NormalizeHistoryCompatibilityData(IEnumerable historyItems) + { + bool normalizedAnyHistoryItems = false; + + foreach (HistoryInfo historyItem in historyItems) + { + if (NormalizeHistoryCompatibilityData(historyItem)) + normalizedAnyHistoryItems = true; + } + + return normalizedAnyHistoryItems; + } + + internal static bool NormalizeHistoryCompatibilityData(HistoryInfo historyItem) + { + (string normalizedLanguageTag, LanguageKind normalizedLanguageKind, bool usedUiAutomation) = + LanguageUtilities.NormalizePersistedLanguageIdentity( + historyItem.LanguageKind, + historyItem.LanguageTag, + historyItem.UsedUiAutomation); + + if (string.Equals(historyItem.LanguageTag, normalizedLanguageTag, StringComparison.Ordinal) + && historyItem.LanguageKind == normalizedLanguageKind + && historyItem.UsedUiAutomation == usedUiAutomation) + { + return false; + } + + historyItem.LanguageTag = normalizedLanguageTag; + historyItem.LanguageKind = normalizedLanguageKind; + historyItem.UsedUiAutomation = usedUiAutomation; + return true; + } + + #endregion Normalization + + #region Word border sidecar files + + internal static bool EnsureWordBorderSidecarFiles(IEnumerable historyItems) + { + bool migratedAnyWordBorderData = false; + + foreach (HistoryInfo historyItem in historyItems) + { + if (PersistWordBorderData(historyItem)) + migratedAnyWordBorderData = true; + } + + return migratedAnyWordBorderData; + } + + internal static void PersistWordBorderData(IEnumerable historyItems) + { + foreach (HistoryInfo historyItem in historyItems) + PersistWordBorderData(historyItem); + } + + internal static bool PersistWordBorderData(HistoryInfo historyItem) + { + if (string.IsNullOrWhiteSpace(historyItem.WordBorderInfoJson)) + return false; + + if (string.IsNullOrWhiteSpace(historyItem.ID)) + historyItem.ID = Guid.NewGuid().ToString(); + + string wordBorderInfoFileName = GetWordBorderInfoFileName(historyItem.ID); + bool couldSaveWordBorderInfo = SaveHistoryTextFileBlocking(historyItem.WordBorderInfoJson, wordBorderInfoFileName); + + if (!couldSaveWordBorderInfo) + { + historyItem.WordBorderInfoFileName = null; + return false; + } + + historyItem.WordBorderInfoFileName = wordBorderInfoFileName; + + // When file-backed settings are enabled, the sidecar file is the authority + // for word border data, so drop the inline JSON to reduce memory/disk usage. + if (SettingsAccess.Current.EnableFileBackedManagedSettings) + historyItem.ClearTransientWordBorderData(); + + return true; + } + + internal static async Task> GetWordBorderInfosAsync(HistoryInfo history) + { + if (!string.IsNullOrWhiteSpace(history.WordBorderInfoFileName)) + { + // Sanitize the persisted file name to prevent path traversal outside the history directory + string sanitizedFileName = Path.GetFileName(history.WordBorderInfoFileName); + + if (!string.IsNullOrWhiteSpace(sanitizedFileName) + && string.Equals(Path.GetExtension(sanitizedFileName), ".json", StringComparison.OrdinalIgnoreCase)) + { + try + { + string historyBasePath = await FileUtilities.GetPathToHistory(); + string wordBorderInfoPath = Path.Combine(historyBasePath, sanitizedFileName); + + if (File.Exists(wordBorderInfoPath)) + { + await using FileStream wordBorderInfoStream = File.OpenRead(wordBorderInfoPath); + List? wordBorderInfos = + await JsonSerializer.DeserializeAsync>(wordBorderInfoStream, HistoryJsonOptions); + + if (wordBorderInfos is not null) + return wordBorderInfos; + } + } + catch (IOException ex) + { + Debug.WriteLine($"Failed to read word border info file for history item '{history.ID}': {ex}"); + } + catch (JsonException ex) + { + Debug.WriteLine($"Failed to deserialize word border info file for history item '{history.ID}': {ex}"); + } + } + } + + if (string.IsNullOrWhiteSpace(history.WordBorderInfoJson)) + return []; + + try + { + List? inlineWordBorderInfos = + JsonSerializer.Deserialize>(history.WordBorderInfoJson, HistoryJsonOptions); + + return inlineWordBorderInfos ?? []; + } + catch (JsonException ex) + { + Debug.WriteLine($"Failed to deserialize inline word border info for history item '{history.ID}': {ex}"); + return []; + } + } + + #endregion Word border sidecar files + + #region Retention + + internal static HistoryInfo? GetMostRecentGrab(IEnumerable historyItems) + { + return historyItems + .Where(history => !history.IsPdfDocument) + .MaxBy(history => history.CaptureDateTime); + } + + internal static List GetExcessVisualHistoryItems(IEnumerable historyItems) + { + return + [ + .. historyItems + .Where(history => !history.IsPdfDocument) + .OrderBy(history => history.CaptureDateTime) + .SkipLast(MaxHistoryWithImages), + .. historyItems + .Where(history => history.IsPdfDocument) + .OrderBy(history => history.CaptureDateTime) + .SkipLast(MaxHistoryPdfDocuments), + ]; + } + + internal static void ClearTransientHistoryPayloads(IEnumerable historyItems) + { + foreach (HistoryInfo historyItem in historyItems) + { + historyItem.ClearTransientImage(); + historyItem.ClearTransientWordBorderData(); + } + } + + #endregion Retention + + #region Deleting artifacts + + internal static void DeleteHistoryArtifacts(HistoryInfo historyItem) + { + DeleteHistoryFile(historyItem.ImagePath); + DeleteHistoryFile(historyItem.WordBorderInfoFileName); + } + + internal static void DeleteUnusedWordBorderFiles(IEnumerable historyItems) + { + string historyBasePath = GetHistoryPathBlocking(); + + if (!Directory.Exists(historyBasePath)) + return; + + HashSet expectedFileNames = [.. historyItems + .Select(historyItem => historyItem.WordBorderInfoFileName) + .Where(fileName => !string.IsNullOrWhiteSpace(fileName)) + .Select(fileName => Path.GetFileName(fileName!))]; + + string[] wordBorderInfoFiles = Directory.GetFiles(historyBasePath, $"*{WordBorderInfoFileSuffix}"); + + foreach (string wordBorderInfoFile in wordBorderInfoFiles) + { + string fileName = Path.GetFileName(wordBorderInfoFile); + + if (!expectedFileNames.Contains(fileName)) + { + try + { + File.Delete(wordBorderInfoFile); + } + catch (IOException ex) + { + Debug.WriteLine($"Failed to delete word border info file '{wordBorderInfoFile}': {ex}"); + } + catch (UnauthorizedAccessException ex) + { + Debug.WriteLine($"Access denied when deleting word border info file '{wordBorderInfoFile}': {ex}"); + } + } + } + } + + private static void DeleteHistoryFile(string? historyFileName) + { + if (string.IsNullOrWhiteSpace(historyFileName)) + return; + + string historyBasePath = GetHistoryPathBlocking(); + string filePath = Path.Combine(historyBasePath, Path.GetFileName(historyFileName)); + + if (!File.Exists(filePath)) + return; + + try + { + File.Delete(filePath); + } + catch (IOException ex) + { + Debug.WriteLine($"Failed to delete history file '{filePath}': {ex}"); + } + catch (UnauthorizedAccessException ex) + { + Debug.WriteLine($"Access denied when deleting history file '{filePath}': {ex}"); + } + } + + #endregion Deleting artifacts + + #region Path and file helpers + + private static string GetHistoryPathBlocking() + { + return Task.Run(async () => await FileUtilities.GetPathToHistory()).GetAwaiter().GetResult(); + } + + private static string GetWordBorderInfoFileName(string historyId) + { + return $"{historyId}{WordBorderInfoFileSuffix}"; + } + + private static bool SaveHistoryTextFileBlocking(string textContent, string fileName) + { + return Task.Run(async () => await FileUtilities.SaveTextFile(textContent, fileName, FileStorageKind.WithHistory)) + .GetAwaiter() + .GetResult(); + } + + #endregion Path and file helpers + + #region Json converter + + private sealed class HistoryLanguageKindJsonConverter : JsonConverter + { + public override LanguageKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + string? value = reader.GetString(); + + if (!string.IsNullOrWhiteSpace(value) + && Enum.TryParse(value, true, out LanguageKind parsedValue) + && Enum.IsDefined(typeof(LanguageKind), parsedValue)) + { + return parsedValue; + } + + HistoryLanguageKindFallbackUsed.Value = true; + Debug.WriteLine($"Unknown history LanguageKind '{value}'. Falling back to {LanguageKind.Global}."); + return LanguageKind.Global; + } + + if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int numericValue)) + { + if (Enum.IsDefined(typeof(LanguageKind), numericValue)) + return (LanguageKind)numericValue; + + HistoryLanguageKindFallbackUsed.Value = true; + Debug.WriteLine($"Unknown history LanguageKind numeric value '{numericValue}'. Falling back to {LanguageKind.Global}."); + return LanguageKind.Global; + } + + if (reader.TokenType == JsonTokenType.Null) + { + HistoryLanguageKindFallbackUsed.Value = true; + return LanguageKind.Global; + } + + HistoryLanguageKindFallbackUsed.Value = true; + Debug.WriteLine($"Unexpected token '{reader.TokenType}' for history LanguageKind. Falling back to {LanguageKind.Global}."); + return LanguageKind.Global; + } + + public override void Write(Utf8JsonWriter writer, LanguageKind value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString()); + } + + #endregion Json converter +} diff --git a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs index 5bf87b72..70346e1b 100644 --- a/Text-Grab.Core/Interfaces/ITextGrabSettings.cs +++ b/Text-Grab.Core/Interfaces/ITextGrabSettings.cs @@ -74,6 +74,9 @@ public interface ITextGrabSettings /// Which local Whisper model to use for on-device audio transcription. string AudioTranscriptionModel { get; set; } + /// Store managed settings and history word borders in files beside the app data. + bool EnableFileBackedManagedSettings { get; set; } + /// Persist pending changes. Backed by ApplicationSettingsBase.Save(). void Save(); } diff --git a/Text-Grab/Services/HistoryService.cs b/Text-Grab/Services/HistoryService.cs index a3e08be0..1317a985 100644 --- a/Text-Grab/Services/HistoryService.cs +++ b/Text-Grab/Services/HistoryService.cs @@ -1,13 +1,9 @@ using Humanizer; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -21,27 +17,23 @@ namespace Text_Grab.Services; +/// +/// The live, app-bound half of the grab history: the in-memory lists, the DispatcherTimer that +/// debounces writes and the one that releases the cache when it goes idle, the cached fullscreen +/// bitmap, the recent-grabs menu, and opening a history entry back into a GrabFrame. +/// +/// Everything that only touches the disk - loading, writing, normalization, the word-border +/// sidecar files and the retention rules - moved to +/// in batch 6e of the Core split. What +/// holds the rest here is state plus WPF: DispatcherTimer and MenuItem are WindowsBase and +/// PresentationFramework, and SaveToHistory takes a GrabFrame and an EditTextWindow. +/// public partial class HistoryService : IDisposable { #region Fields - private static readonly int maxHistoryTextOnly = 100; - private static readonly int maxHistoryWithImages = 10; - private static readonly int maxHistoryPdfDocuments = 10; - private const string WordBorderInfoFileSuffix = ".wordborders.json"; private static readonly TimeSpan historyCacheCheckInterval = TimeSpan.FromMinutes(1); private static readonly TimeSpan historyCacheIdleLifetime = TimeSpan.FromMinutes(2); - private static readonly AsyncLocal HistoryLanguageKindFallbackUsed = new(); - private static readonly JsonSerializerOptions HistoryJsonOptions = new() - { - AllowTrailingCommas = true, - WriteIndented = true, - Converters = - { - new HistoryLanguageKindJsonConverter(), - new JsonStringEnumConverter() - } - }; private List HistoryTextOnly = []; private List HistoryWithImage = []; private readonly DispatcherTimer saveTimer = new(); @@ -123,7 +115,7 @@ public bool GetLastHistoryAsGrabFrame() { EnsureImageHistoryLoaded(); TouchHistoryCache(); - HistoryInfo? lastHistoryItem = GetMostRecentGrab(HistoryWithImage); + HistoryInfo? lastHistoryItem = HistoryFileUtilities.GetMostRecentGrab(HistoryWithImage); if (lastHistoryItem is not HistoryInfo historyInfo) return false; @@ -135,13 +127,6 @@ public bool GetLastHistoryAsGrabFrame() return true; } - internal static HistoryInfo? GetMostRecentGrab(IEnumerable historyItems) - { - return historyItems - .Where(history => !history.IsPdfDocument) - .MaxBy(history => history.CaptureDateTime); - } - public string GetLastTextHistory() { EnsureTextHistoryLoaded(); @@ -182,19 +167,27 @@ public async Task LoadHistories() _hasPendingWrite = false; ReleaseLoadedHistoriesCore(); - (HistoryTextOnly, bool textHistoryNeedsRewrite) = await LoadHistoryAsync(nameof(HistoryTextOnly)); + (HistoryTextOnly, bool textHistoryNeedsRewrite) = + await HistoryFileUtilities.LoadHistoryAsync(nameof(HistoryTextOnly)); _textHistoryLoaded = true; - NormalizeHistoryIds(HistoryTextOnly); - if (textHistoryNeedsRewrite || NormalizeHistoryCompatibilityData(HistoryTextOnly)) + // Both normalizers mutate, so neither may be short-circuited away by the other. + bool normalizedTextIds = HistoryFileUtilities.NormalizeHistoryIds(HistoryTextOnly); + bool normalizedTextCompatibilityData = HistoryFileUtilities.NormalizeHistoryCompatibilityData(HistoryTextOnly); + + if (normalizedTextIds || textHistoryNeedsRewrite || normalizedTextCompatibilityData) MarkHistoryDirty(); - (HistoryWithImage, bool imageHistoryNeedsRewrite) = await LoadHistoryAsync(nameof(HistoryWithImage)); + (HistoryWithImage, bool imageHistoryNeedsRewrite) = + await HistoryFileUtilities.LoadHistoryAsync(nameof(HistoryWithImage)); _imageHistoryLoaded = true; - NormalizeHistoryIds(HistoryWithImage); - if (imageHistoryNeedsRewrite || NormalizeHistoryCompatibilityData(HistoryWithImage)) + // Both normalizers mutate, so neither may be short-circuited away by the other. + bool normalizedImageIds = HistoryFileUtilities.NormalizeHistoryIds(HistoryWithImage); + bool normalizedImageCompatibilityData = HistoryFileUtilities.NormalizeHistoryCompatibilityData(HistoryWithImage); + + if (normalizedImageIds || imageHistoryNeedsRewrite || normalizedImageCompatibilityData) MarkHistoryDirty(); - if (EnsureWordBorderSidecarFiles(HistoryWithImage)) + if (HistoryFileUtilities.EnsureWordBorderSidecarFiles(HistoryWithImage)) MarkHistoryDirty(); TouchHistoryCache(); @@ -319,8 +312,8 @@ public void SaveToHistory(GrabFrame grabFrameToSave) if (string.IsNullOrEmpty(historyInfo.ID)) historyInfo.ID = Guid.NewGuid().ToString(); - NormalizeHistoryCompatibilityData(historyInfo); - PersistWordBorderData(historyInfo); + HistoryFileUtilities.NormalizeHistoryCompatibilityData(historyInfo); + HistoryFileUtilities.PersistWordBorderData(historyInfo); if (historyInfo.ImageContent is not null && !string.IsNullOrWhiteSpace(historyInfo.ImagePath)) FileUtilities.SaveImageFile(historyInfo.ImageContent, historyInfo.ImagePath, FileStorageKind.WithHistory); @@ -348,8 +341,8 @@ public void SaveToHistory(HistoryInfo infoFromFullscreenGrab) infoFromFullscreenGrab.ImagePath = $"{imgRandomName}.bmp"; - NormalizeHistoryCompatibilityData(infoFromFullscreenGrab); - PersistWordBorderData(infoFromFullscreenGrab); + HistoryFileUtilities.NormalizeHistoryCompatibilityData(infoFromFullscreenGrab); + HistoryFileUtilities.PersistWordBorderData(infoFromFullscreenGrab); infoFromFullscreenGrab.ClearTransientImage(); HistoryWithImage.Add(infoFromFullscreenGrab); @@ -366,7 +359,7 @@ public void SaveToHistory(EditTextWindow etwToSave) EnsureTextHistoryLoaded(); TouchHistoryCache(); HistoryInfo historyInfo = etwToSave.AsHistoryItem(); - NormalizeHistoryCompatibilityData(historyInfo); + HistoryFileUtilities.NormalizeHistoryCompatibilityData(historyInfo); foreach (HistoryInfo inHistoryItem in HistoryTextOnly) { @@ -393,20 +386,23 @@ public void WriteHistory() if (_textHistoryLoaded) { - NormalizeHistoryCompatibilityData(HistoryTextOnly); - WriteHistoryFiles(HistoryTextOnly, nameof(HistoryTextOnly), maxHistoryTextOnly); + HistoryFileUtilities.NormalizeHistoryCompatibilityData(HistoryTextOnly); + HistoryFileUtilities.WriteHistoryFiles( + HistoryTextOnly, + nameof(HistoryTextOnly), + HistoryFileUtilities.MaxHistoryTextOnly); } if (_imageHistoryLoaded) { ClearOldImages(); - NormalizeHistoryCompatibilityData(HistoryWithImage); - PersistWordBorderData(HistoryWithImage); - WriteHistoryFiles( + HistoryFileUtilities.NormalizeHistoryCompatibilityData(HistoryWithImage); + HistoryFileUtilities.PersistWordBorderData(HistoryWithImage); + HistoryFileUtilities.WriteHistoryFiles( HistoryWithImage, nameof(HistoryWithImage), - maxHistoryWithImages + maxHistoryPdfDocuments); - DeleteUnusedWordBorderFiles(HistoryWithImage); + HistoryFileUtilities.MaxHistoryWithImages + HistoryFileUtilities.MaxHistoryPdfDocuments); + HistoryFileUtilities.DeleteUnusedWordBorderFiles(HistoryWithImage); } _hasPendingWrite = false; @@ -428,7 +424,7 @@ public void RemoveImageHistoryItem(HistoryInfo historyItem) HistoryWithImage.Remove(historyItem); historyItem.ClearTransientImage(); historyItem.ClearTransientWordBorderData(); - DeleteHistoryArtifacts(historyItem); + HistoryFileUtilities.DeleteHistoryArtifacts(historyItem); MarkHistoryDirty(); } @@ -453,59 +449,10 @@ public void RemoveImageHistoryItem(HistoryInfo historyItem) return HistoryTextOnly.FirstOrDefault(history => history.ID == historyId); } - public async Task> GetWordBorderInfosAsync(HistoryInfo history) + public Task> GetWordBorderInfosAsync(HistoryInfo history) { TouchHistoryCache(); - - if (!string.IsNullOrWhiteSpace(history.WordBorderInfoFileName)) - { - // Sanitize the persisted file name to prevent path traversal outside the history directory - string sanitizedFileName = Path.GetFileName(history.WordBorderInfoFileName); - - if (!string.IsNullOrWhiteSpace(sanitizedFileName) - && string.Equals(Path.GetExtension(sanitizedFileName), ".json", StringComparison.OrdinalIgnoreCase)) - { - try - { - string historyBasePath = await FileUtilities.GetPathToHistory(); - string wordBorderInfoPath = Path.Combine(historyBasePath, sanitizedFileName); - - if (File.Exists(wordBorderInfoPath)) - { - await using FileStream wordBorderInfoStream = File.OpenRead(wordBorderInfoPath); - List? wordBorderInfos = - await JsonSerializer.DeserializeAsync>(wordBorderInfoStream, HistoryJsonOptions); - - if (wordBorderInfos is not null) - return wordBorderInfos; - } - } - catch (IOException ex) - { - Debug.WriteLine($"Failed to read word border info file for history item '{history.ID}': {ex}"); - } - catch (JsonException ex) - { - Debug.WriteLine($"Failed to deserialize word border info file for history item '{history.ID}': {ex}"); - } - } - } - - if (string.IsNullOrWhiteSpace(history.WordBorderInfoJson)) - return []; - - try - { - List? inlineWordBorderInfos = - JsonSerializer.Deserialize>(history.WordBorderInfoJson, HistoryJsonOptions); - - return inlineWordBorderInfos ?? []; - } - catch (JsonException ex) - { - Debug.WriteLine($"Failed to deserialize inline word border info for history item '{history.ID}': {ex}"); - return []; - } + return HistoryFileUtilities.GetWordBorderInfosAsync(history); } public void ReleaseLoadedHistories() @@ -542,101 +489,9 @@ public void Dispose() #region Private Methods - private static async Task<(List HistoryItems, bool NeedsRewrite)> LoadHistoryAsync(string fileName) - { - string rawText = await FileUtilities.GetTextFileAsync($"{fileName}.json", FileStorageKind.WithHistory); - - if (string.IsNullOrWhiteSpace(rawText)) - return ([], false); - - try - { - HistoryLanguageKindFallbackUsed.Value = false; - List? tempHistory = JsonSerializer.Deserialize>(rawText, HistoryJsonOptions); - - if (tempHistory is List jsonList && jsonList.Count > 0) - return (tempHistory, HistoryLanguageKindFallbackUsed.Value); - } - catch (JsonException ex) - { - Debug.WriteLine($"Failed to deserialize history file '{fileName}.json' as a list. Attempting item-by-item recovery. {ex}"); - return LoadHistoryWithRecovery(rawText, fileName); - } - finally - { - HistoryLanguageKindFallbackUsed.Value = false; - } - - return ([], false); - } - - private static (List HistoryItems, bool NeedsRewrite) LoadHistoryWithRecovery(string rawText, string fileName) - { - try - { - using JsonDocument document = JsonDocument.Parse(rawText); - - if (document.RootElement.ValueKind != JsonValueKind.Array) - return ([], true); - - List recoveredHistory = []; - bool needsRewrite = true; - int index = 0; - - foreach (JsonElement element in document.RootElement.EnumerateArray()) - { - try - { - HistoryLanguageKindFallbackUsed.Value = false; - HistoryInfo? historyItem = element.Deserialize(HistoryJsonOptions); - if (historyItem is not null) - { - recoveredHistory.Add(historyItem); - if (HistoryLanguageKindFallbackUsed.Value) - needsRewrite = true; - } - } - catch (JsonException ex) - { - Debug.WriteLine($"Skipped invalid history item at index {index} from '{fileName}.json'. {ex}"); - } - finally - { - HistoryLanguageKindFallbackUsed.Value = false; - } - - index++; - } - - return (recoveredHistory, needsRewrite); - } - catch (JsonException ex) - { - Debug.WriteLine($"Failed to parse history file '{fileName}.json' during recovery. {ex}"); - return ([], true); - } - } - - private static void WriteHistoryFiles(List history, string fileName, int maxNumberToSave) - { - string historyAsJson = JsonSerializer - .Serialize(history - .OrderBy(x => x.CaptureDateTime) - .TakeLast(maxNumberToSave), - HistoryJsonOptions); - - try - { - SaveHistoryTextFileBlocking(historyAsJson, $"{fileName}.json"); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to save history json file. {ex.Message}"); - } - } private void ClearOldImages() { - List imagesToRemove = GetExcessVisualHistoryItems(HistoryWithImage); + List imagesToRemove = HistoryFileUtilities.GetExcessVisualHistoryItems(HistoryWithImage); if (imagesToRemove.Count == 0) return; @@ -645,24 +500,9 @@ private void ClearOldImages() HistoryWithImage.Remove(historyItem); foreach (HistoryInfo infoItem in imagesToRemove) - DeleteHistoryArtifacts(infoItem); - - ClearTransientHistoryPayloads(imagesToRemove); - } + HistoryFileUtilities.DeleteHistoryArtifacts(infoItem); - internal static List GetExcessVisualHistoryItems(IEnumerable historyItems) - { - return - [ - .. historyItems - .Where(history => !history.IsPdfDocument) - .OrderBy(history => history.CaptureDateTime) - .SkipLast(maxHistoryWithImages), - .. historyItems - .Where(history => history.IsPdfDocument) - .OrderBy(history => history.CaptureDateTime) - .SkipLast(maxHistoryPdfDocuments), - ]; + HistoryFileUtilities.ClearTransientHistoryPayloads(imagesToRemove); } private void DisposeCachedBitmap() @@ -677,27 +517,22 @@ private void DisposeCachedBitmap() CachedBitmap = null; } - private static void ClearTransientHistoryPayloads(IEnumerable historyItems) - { - foreach (HistoryInfo historyItem in historyItems) - { - historyItem.ClearTransientImage(); - historyItem.ClearTransientWordBorderData(); - } - } - private void EnsureImageHistoryLoaded() { if (_imageHistoryLoaded) return; - (HistoryWithImage, bool imageHistoryNeedsRewrite) = LoadHistoryBlocking(nameof(HistoryWithImage)); + (HistoryWithImage, bool imageHistoryNeedsRewrite) = + HistoryFileUtilities.LoadHistoryBlocking(nameof(HistoryWithImage)); _imageHistoryLoaded = true; - NormalizeHistoryIds(HistoryWithImage); - if (imageHistoryNeedsRewrite || NormalizeHistoryCompatibilityData(HistoryWithImage)) + // Both normalizers mutate, so neither may be short-circuited away by the other. + bool normalizedIds = HistoryFileUtilities.NormalizeHistoryIds(HistoryWithImage); + bool normalizedCompatibilityData = HistoryFileUtilities.NormalizeHistoryCompatibilityData(HistoryWithImage); + + if (normalizedIds || imageHistoryNeedsRewrite || normalizedCompatibilityData) MarkHistoryDirty(); - if (EnsureWordBorderSidecarFiles(HistoryWithImage)) + if (HistoryFileUtilities.EnsureWordBorderSidecarFiles(HistoryWithImage)) MarkHistoryDirty(); } @@ -706,10 +541,14 @@ private void EnsureTextHistoryLoaded() if (_textHistoryLoaded) return; - (HistoryTextOnly, bool textHistoryNeedsRewrite) = LoadHistoryBlocking(nameof(HistoryTextOnly)); + (HistoryTextOnly, bool textHistoryNeedsRewrite) = + HistoryFileUtilities.LoadHistoryBlocking(nameof(HistoryTextOnly)); _textHistoryLoaded = true; - NormalizeHistoryIds(HistoryTextOnly); - if (textHistoryNeedsRewrite || NormalizeHistoryCompatibilityData(HistoryTextOnly)) + // Both normalizers mutate, so neither may be short-circuited away by the other. + bool normalizedIds = HistoryFileUtilities.NormalizeHistoryIds(HistoryTextOnly); + bool normalizedCompatibilityData = HistoryFileUtilities.NormalizeHistoryCompatibilityData(HistoryTextOnly); + + if (normalizedIds || textHistoryNeedsRewrite || normalizedCompatibilityData) MarkHistoryDirty(); } @@ -727,95 +566,6 @@ private void HistoryCacheReleaseTimer_Tick(object? sender, EventArgs e) ReleaseLoadedHistoriesCore(); } - private static (List HistoryItems, bool NeedsRewrite) LoadHistoryBlocking(string fileName) - { - return Task.Run(() => LoadHistoryAsync(fileName)).GetAwaiter().GetResult(); - } - - private static string GetHistoryPathBlocking() - { - return Task.Run(async () => await FileUtilities.GetPathToHistory()).GetAwaiter().GetResult(); - } - - private static string GetWordBorderInfoFileName(string historyId) - { - return $"{historyId}{WordBorderInfoFileSuffix}"; - } - - private static bool SaveHistoryTextFileBlocking(string textContent, string fileName) - { - return Task.Run(async () => await FileUtilities.SaveTextFile(textContent, fileName, FileStorageKind.WithHistory)) - .GetAwaiter() - .GetResult(); - } - - private void DeleteHistoryArtifacts(HistoryInfo historyItem) - { - DeleteHistoryFile(historyItem.ImagePath); - DeleteHistoryFile(historyItem.WordBorderInfoFileName); - } - - private static void DeleteHistoryFile(string? historyFileName) - { - if (string.IsNullOrWhiteSpace(historyFileName)) - return; - - string historyBasePath = GetHistoryPathBlocking(); - string filePath = Path.Combine(historyBasePath, Path.GetFileName(historyFileName)); - - if (!File.Exists(filePath)) - return; - - try - { - File.Delete(filePath); - } - catch (IOException ex) - { - Debug.WriteLine($"Failed to delete history file '{filePath}': {ex}"); - } - catch (UnauthorizedAccessException ex) - { - Debug.WriteLine($"Access denied when deleting history file '{filePath}': {ex}"); - } - } - - private void DeleteUnusedWordBorderFiles(IEnumerable historyItems) - { - string historyBasePath = GetHistoryPathBlocking(); - - if (!Directory.Exists(historyBasePath)) - return; - - HashSet expectedFileNames = [.. historyItems - .Select(historyItem => historyItem.WordBorderInfoFileName) - .Where(fileName => !string.IsNullOrWhiteSpace(fileName)) - .Select(fileName => Path.GetFileName(fileName!))]; - - string[] wordBorderInfoFiles = Directory.GetFiles(historyBasePath, $"*{WordBorderInfoFileSuffix}"); - - foreach (string wordBorderInfoFile in wordBorderInfoFiles) - { - string fileName = Path.GetFileName(wordBorderInfoFile); - - if (!expectedFileNames.Contains(fileName)) - { - try - { - File.Delete(wordBorderInfoFile); - } - catch (IOException ex) - { - Debug.WriteLine($"Failed to delete word border info file '{wordBorderInfoFile}': {ex}"); - } - catch (UnauthorizedAccessException ex) - { - Debug.WriteLine($"Access denied when deleting word border info file '{wordBorderInfoFile}': {ex}"); - } - } - } - } - private void MarkHistoryDirty() { _hasPendingWrite = true; @@ -824,114 +574,9 @@ private void MarkHistoryDirty() saveTimer.Start(); } - private bool EnsureWordBorderSidecarFiles(IEnumerable historyItems) - { - bool migratedAnyWordBorderData = false; - - foreach (HistoryInfo historyItem in historyItems) - { - if (PersistWordBorderData(historyItem)) - migratedAnyWordBorderData = true; - } - - return migratedAnyWordBorderData; - } - - private static bool NormalizeHistoryCompatibilityData(IEnumerable historyItems) - { - bool normalizedAnyHistoryItems = false; - - foreach (HistoryInfo historyItem in historyItems) - { - if (NormalizeHistoryCompatibilityData(historyItem)) - normalizedAnyHistoryItems = true; - } - - return normalizedAnyHistoryItems; - } - - private static bool NormalizeHistoryCompatibilityData(HistoryInfo historyItem) - { - (string normalizedLanguageTag, LanguageKind normalizedLanguageKind, bool usedUiAutomation) = - LanguageUtilities.NormalizePersistedLanguageIdentity( - historyItem.LanguageKind, - historyItem.LanguageTag, - historyItem.UsedUiAutomation); - - if (string.Equals(historyItem.LanguageTag, normalizedLanguageTag, StringComparison.Ordinal) - && historyItem.LanguageKind == normalizedLanguageKind - && historyItem.UsedUiAutomation == usedUiAutomation) - { - return false; - } - - historyItem.LanguageTag = normalizedLanguageTag; - historyItem.LanguageKind = normalizedLanguageKind; - historyItem.UsedUiAutomation = usedUiAutomation; - return true; - } - - private void PersistWordBorderData(IEnumerable historyItems) - { - foreach (HistoryInfo historyItem in historyItems) - PersistWordBorderData(historyItem); - } - - private bool PersistWordBorderData(HistoryInfo historyItem) - { - if (string.IsNullOrWhiteSpace(historyItem.WordBorderInfoJson)) - return false; - - if (string.IsNullOrWhiteSpace(historyItem.ID)) - historyItem.ID = Guid.NewGuid().ToString(); - - string wordBorderInfoFileName = GetWordBorderInfoFileName(historyItem.ID); - bool couldSaveWordBorderInfo = SaveHistoryTextFileBlocking(historyItem.WordBorderInfoJson, wordBorderInfoFileName); - - if (!couldSaveWordBorderInfo) - { - historyItem.WordBorderInfoFileName = null; - return false; - } - - historyItem.WordBorderInfoFileName = wordBorderInfoFileName; - - // When file-backed settings are enabled, the sidecar file is the authority - // for word border data, so drop the inline JSON to reduce memory/disk usage. - if (DefaultSettings.EnableFileBackedManagedSettings) - historyItem.ClearTransientWordBorderData(); - - return true; - } - - private void NormalizeHistoryIds(List historyItems) - { - HashSet seenIds = []; - bool updatedAnyIds = false; - - foreach (HistoryInfo historyItem in historyItems) - { - if (!string.IsNullOrWhiteSpace(historyItem.ID) && seenIds.Add(historyItem.ID)) - continue; - - string nextId; - do - { - nextId = Guid.NewGuid().ToString(); - } - while (!seenIds.Add(nextId)); - - historyItem.ID = nextId; - updatedAnyIds = true; - } - - if (updatedAnyIds) - MarkHistoryDirty(); - } - private void ReleaseLoadedHistoriesCore() { - ClearTransientHistoryPayloads(HistoryWithImage); + HistoryFileUtilities.ClearTransientHistoryPayloads(HistoryWithImage); HistoryWithImage.Clear(); HistoryTextOnly.Clear(); _imageHistoryLoaded = false; @@ -955,50 +600,5 @@ private void TouchHistoryCache() historyCacheReleaseTimer.Start(); } - private sealed class HistoryLanguageKindJsonConverter : JsonConverter - { - public override LanguageKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - string? value = reader.GetString(); - - if (!string.IsNullOrWhiteSpace(value) - && Enum.TryParse(value, true, out LanguageKind parsedValue) - && Enum.IsDefined(typeof(LanguageKind), parsedValue)) - { - return parsedValue; - } - - HistoryLanguageKindFallbackUsed.Value = true; - Debug.WriteLine($"Unknown history LanguageKind '{value}'. Falling back to {LanguageKind.Global}."); - return LanguageKind.Global; - } - - if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int numericValue)) - { - if (Enum.IsDefined(typeof(LanguageKind), numericValue)) - return (LanguageKind)numericValue; - - HistoryLanguageKindFallbackUsed.Value = true; - Debug.WriteLine($"Unknown history LanguageKind numeric value '{numericValue}'. Falling back to {LanguageKind.Global}."); - return LanguageKind.Global; - } - - if (reader.TokenType == JsonTokenType.Null) - { - HistoryLanguageKindFallbackUsed.Value = true; - return LanguageKind.Global; - } - - HistoryLanguageKindFallbackUsed.Value = true; - Debug.WriteLine($"Unexpected token '{reader.TokenType}' for history LanguageKind. Falling back to {LanguageKind.Global}."); - return LanguageKind.Global; - } - - public override void Write(Utf8JsonWriter writer, LanguageKind value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString()); - } - #endregion Private Methods } diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index 085cf2ce..20beecba 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -1,4 +1,4 @@ -using Humanizer; +using Humanizer; using System; using System.Collections.Generic; using System.ComponentModel; diff --git a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs index 395c1957..602948fa 100644 --- a/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs +++ b/Text-Grab/Views/FullscreenGrab.SelectionStyles.cs @@ -1,4 +1,4 @@ -using Dapplo.Windows.User32; +using Dapplo.Windows.User32; using System; using System.Collections.Generic; using System.Linq; From 9f7869432ce26f96f83fdb980c7945f2121599be Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:09:10 -0500 Subject: [PATCH 44/54] Plan: record 6e and the HistoryInfo move; reopen two section 7 rows Documents what 6e actually did rather than what it forecast, including the two behaviour-preserving details (NormalizeHistoryIds returning a bool instead of calling MarkHistoryDirty, and why GetWordBorderInfosAsync kept a wrapper). HistoryInfo landing in Core.Windows also removes the root blocker under the GrabFrameFileUtilities and FileAssociationUtilities rows, so both are rewritten rather than left claiming a blocker that no longer exists. The HistoryService row is gone - resolved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- docs/Core-Split-Plan.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 698ddb5a..4d21bb46 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -592,6 +592,42 @@ JSON pipeline (`LoadHistoryAsync`, `LoadHistoryWithRecovery`, `WriteHistoryFiles both halves. Blocked on `HistoryInfo`'s own `System.Windows.Rect PositionRect` — which B2 and 2d now give a path to. +**`HistoryInfo` first, in `a8591aa`.** `PositionRect` was never a stored field — it is a +projection over the persisted `RectAsString`, and only `RectAsString` is serialized — so B2's +currency change costs nothing on disk. It is now a `System.Drawing.RectangleF` with hand-rolled +parse/format helpers that keep the `"x,y,width,height"` text `Rect.ToString()` wrote, plus the +literal `"Empty"`. Writing is invariant-culture; reading additionally tolerates the `';'` +separator and comma decimals `Rect.ToString()` emitted under cultures whose decimal separator is +`','` — strings the old invariant-only `Rect.Parse` threw on rather than read. The eleven call +sites convert at the edge through `ShapeExtensions`. `HistoryInfo.cs` then moved to +`Text-Grab.Core.Windows/Models/` with no other edit, which also clears the root blocker under the +`GrabFrameFileUtilities` and `FileAssociationUtilities` §7 rows. + +**6e as executed, in `212234a`.** `Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs` takes +the whole persistence pipeline: `LoadHistoryAsync` / `LoadHistoryBlocking` / +`LoadHistoryWithRecovery` / `WriteHistoryFiles`, `HistoryLanguageKindJsonConverter` and the +`AsyncLocal` rewrite flag it sets, `NormalizeHistoryIds` and both +`NormalizeHistoryCompatibilityData` overloads, the word-border sidecar chain +(`EnsureWordBorderSidecarFiles`, both `PersistWordBorderData` overloads, +`GetWordBorderInfosAsync`), retention (`GetMostRecentGrab`, `GetExcessVisualHistoryItems`, the +three `Max*` caps, `ClearTransientHistoryPayloads`) and artifact deletion. All static, no state +past the serializer options. + +`HistoryService` kept its name — every call site is `Singleton.Instance` — and +kept what actually held it in the app: the two `List` fields, the two +`DispatcherTimer`s that debounce writes and release the idle cache, the cached fullscreen +`Bitmap` and its HBITMAP, the recent-grabs `MenuItem` building, and the `SaveToHistory` overloads +taking `GrabFrame` and `EditTextWindow`. + +Two behaviour-preserving details. `NormalizeHistoryIds` used to call `MarkHistoryDirty` itself; it +returns a `bool` now so it can be static, and its four callers evaluate it and +`NormalizeHistoryCompatibilityData` into locals before testing them — both normalizers mutate, so +neither may be short-circuited away by the other, which the obvious `||` chain would have done. +`GetWordBorderInfosAsync` stays on the service as a two-line wrapper, because the +`TouchHistoryCache()` it opens with is cache bookkeeping, not file work. +`PersistWordBorderData` reads `EnableFileBackedManagedSettings`, so that joined +`ITextGrabSettings` (member 20). + ### 4.7 Wave 7 — tests and closeout **7a — test migration.** To `Tests.Core`: `StringMethodTests`, `TextSearchUtilitiesTests`, @@ -781,13 +817,12 @@ Every row below was verified by reading the file, not inferred. | File | Blocker (specific) | Unblocked by | |---|---|---| | `Utilities/OcrSourceUtilities.cs` | Post-4c remainder. `LoadBitmapFromFile` builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite, and it takes `OcrAbsoluteFilePathAsync` and `OcrFile` with it. Engine dispatch additionally needs `WindowsAiUtilities` (5a) and `LanguageUtilities` (4e); the rest is `Window`/`BitmapSource` capture and stays | a GDI+/WIC rewrite, then 5a + 4e | -| `Utilities/FileAssociationUtilities.cs` | `GrabFrameExtensionKeyPath` is a `const` field initializer referencing `GrabFrameFileUtilities.GrabFrameFileExtension`, which requires the type at compile time. `GrabFrameFileUtilities` stays app-side (see its own row below), so this file does too - the same root blocker, one level removed | `GrabFrameFileUtilities` moving (`HistoryInfo`) | -| `Utilities/GrabFrameFileUtilities.cs` | Public signature bound to `HistoryInfo`, which needs B2 (`Rect PositionRect`), 2a (five enums) and 4e. A façade was considered and rejected as larger than the file it wraps | 2d + 4e, or `HistoryInfo` moving | +| `Utilities/FileAssociationUtilities.cs` | `GrabFrameExtensionKeyPath` is a `const` field initializer referencing `GrabFrameFileUtilities.GrabFrameFileExtension`, which requires the type at compile time. Still one level removed from the same root blocker, but that blocker is gone: it now follows `GrabFrameFileUtilities` whenever that moves | `GrabFrameFileUtilities` moving | +| `Utilities/GrabFrameFileUtilities.cs` | **Unblocked by `a8591aa`** — `HistoryInfo` is in Core.Windows now, so the public signature no longer pins this file to the app. Re-derive what is left of it before moving; it was never audited past the `HistoryInfo` blocker | nothing known — ready to attempt | | `Services/SettingsService.cs` | Clones `ButtonInfo` and `ShortcutKeySet` field-by-field (both never-move); `Windows.Storage.ApplicationDataContainer` caps it at Core.Windows regardless | needs a `ButtonInfo` redesign — likely never | | `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so the remaining blocker is a plain split | a split | | `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam. `GrabTemplate`/`TemplateRegion` moved to Core in 2d and 4c has landed, so the remaining blocker is the settings façade plus its calls into `OcrSourceUtilities` | a façade + `OcrSourceUtilities` | | `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | -| `Services/HistoryService.cs` | Headless JSON pipeline interleaved with WPF menu building and `GrabFrame`/`EditTextWindow` construction, sharing private state | Opus split (6e) | ## 8. Verified dead code — free, zero-risk prep From a3643e6eb99f2ae8902dcc96c58f42ec184e8c43 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:46:35 -0500 Subject: [PATCH 45/54] 7a: move seven pure model/utility test suites to Tests.Core Straightforward moves for batch 7a - no production code, no test code, and no csproj changed besides the namespace line (Tests -> Text_Grab.Tests.Core). Each suite exercises only types already living in Text-Grab.Core: StringMethodTests, TextSearchUtilitiesTests, ColumnSplitUtilitiesTests, SpreadsheetUndoHistoryTests, EditTextTableDocumentTests, GrabFrameTableEditStateTests, ExtractedPatternTests. Verified each file's usings against invariant 8 before moving - none touch SettingsAccess, a [Collection] fixture, or any Windows-only type. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- {Tests => Tests.Core}/ColumnSplitUtilitiesTests.cs | 2 +- {Tests => Tests.Core}/EditTextTableDocumentTests.cs | 2 +- {Tests => Tests.Core}/ExtractedPatternTests.cs | 2 +- {Tests => Tests.Core}/GrabFrameTableEditStateTests.cs | 2 +- {Tests => Tests.Core}/SpreadsheetUndoHistoryTests.cs | 2 +- {Tests => Tests.Core}/StringMethodTests.cs | 2 +- {Tests => Tests.Core}/TextSearchUtilitiesTests.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename {Tests => Tests.Core}/ColumnSplitUtilitiesTests.cs (99%) rename {Tests => Tests.Core}/EditTextTableDocumentTests.cs (99%) rename {Tests => Tests.Core}/ExtractedPatternTests.cs (99%) rename {Tests => Tests.Core}/GrabFrameTableEditStateTests.cs (96%) rename {Tests => Tests.Core}/SpreadsheetUndoHistoryTests.cs (98%) rename {Tests => Tests.Core}/StringMethodTests.cs (99%) rename {Tests => Tests.Core}/TextSearchUtilitiesTests.cs (98%) diff --git a/Tests/ColumnSplitUtilitiesTests.cs b/Tests.Core/ColumnSplitUtilitiesTests.cs similarity index 99% rename from Tests/ColumnSplitUtilitiesTests.cs rename to Tests.Core/ColumnSplitUtilitiesTests.cs index c6d1071e..81ca4cb8 100644 --- a/Tests/ColumnSplitUtilitiesTests.cs +++ b/Tests.Core/ColumnSplitUtilitiesTests.cs @@ -1,7 +1,7 @@ using Text_Grab.Models; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; public class ColumnSplitUtilitiesTests { diff --git a/Tests/EditTextTableDocumentTests.cs b/Tests.Core/EditTextTableDocumentTests.cs similarity index 99% rename from Tests/EditTextTableDocumentTests.cs rename to Tests.Core/EditTextTableDocumentTests.cs index d81f228a..bd0223cf 100644 --- a/Tests/EditTextTableDocumentTests.cs +++ b/Tests.Core/EditTextTableDocumentTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Text_Grab.Models; -namespace Tests; +namespace Text_Grab.Tests.Core; public class EditTextTableDocumentTests { diff --git a/Tests/ExtractedPatternTests.cs b/Tests.Core/ExtractedPatternTests.cs similarity index 99% rename from Tests/ExtractedPatternTests.cs rename to Tests.Core/ExtractedPatternTests.cs index b0e97d48..c585027f 100644 --- a/Tests/ExtractedPatternTests.cs +++ b/Tests.Core/ExtractedPatternTests.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; using Text_Grab.Models; -namespace Tests; +namespace Text_Grab.Tests.Core; public class ExtractedPatternTests { diff --git a/Tests/GrabFrameTableEditStateTests.cs b/Tests.Core/GrabFrameTableEditStateTests.cs similarity index 96% rename from Tests/GrabFrameTableEditStateTests.cs rename to Tests.Core/GrabFrameTableEditStateTests.cs index cd0ffe2e..2a131958 100644 --- a/Tests/GrabFrameTableEditStateTests.cs +++ b/Tests.Core/GrabFrameTableEditStateTests.cs @@ -1,6 +1,6 @@ using Text_Grab.Models; -namespace Tests; +namespace Text_Grab.Tests.Core; public class GrabFrameTableEditStateTests { diff --git a/Tests/SpreadsheetUndoHistoryTests.cs b/Tests.Core/SpreadsheetUndoHistoryTests.cs similarity index 98% rename from Tests/SpreadsheetUndoHistoryTests.cs rename to Tests.Core/SpreadsheetUndoHistoryTests.cs index 4dd9b6a9..3e57c359 100644 --- a/Tests/SpreadsheetUndoHistoryTests.cs +++ b/Tests.Core/SpreadsheetUndoHistoryTests.cs @@ -1,6 +1,6 @@ using Text_Grab.Models; -namespace Tests; +namespace Text_Grab.Tests.Core; public class SpreadsheetUndoHistoryTests { diff --git a/Tests/StringMethodTests.cs b/Tests.Core/StringMethodTests.cs similarity index 99% rename from Tests/StringMethodTests.cs rename to Tests.Core/StringMethodTests.cs index da6a0b87..4ccbf2e2 100644 --- a/Tests/StringMethodTests.cs +++ b/Tests.Core/StringMethodTests.cs @@ -4,7 +4,7 @@ using Text_Grab; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; public class StringMethodTests { diff --git a/Tests/TextSearchUtilitiesTests.cs b/Tests.Core/TextSearchUtilitiesTests.cs similarity index 98% rename from Tests/TextSearchUtilitiesTests.cs rename to Tests.Core/TextSearchUtilitiesTests.cs index 70abc954..8dec67e2 100644 --- a/Tests/TextSearchUtilitiesTests.cs +++ b/Tests.Core/TextSearchUtilitiesTests.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; public class TextSearchUtilitiesTests { From 873ea58e11e5cb75194190476abefb1b397ba01f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:47:03 -0500 Subject: [PATCH 46/54] 7a: move five headless-Windows test suites to Tests.Core.Windows LanguageTests, WindowsAiUtilitiesTests, QrCodeTests, HdrScreenCaptureTests and ImageChangeDetectorTests move unsplit - each exercises only types already in Text-Grab.Core.Windows (TessLang/GlobalLang/WindowsAiLang, WindowsAiUtilities.CleanRegexResult, BarcodeUtilities' SVG path, HdrScreenCapture.BuildCaptureSegments, ImageChangeDetector) with no settings, collection fixture, or WPF touch. Namespace changed to Text_Grab.Tests.Core.Windows; no other code changed. Tests.Core.Windows.csproj gained two asset items: FontTest.png and font_sample.png stay physically in Tests\Images (Tests\ImageMethodsTests.cs, Tests\OcrSourceTests.cs and Tests\FilesIoTests.cs still read them there) but are linked in here for ImageChangeDetectorTests, which reads them by the same relative path FileUtilities.GetPathToLocalFile resolves at either project's output directory. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- .../HdrScreenCaptureTests.cs | 2 +- .../ImageChangeDetectorTests.cs | 2 +- .../LanguageTests.cs | 2 +- {Tests => Tests.Core.Windows}/QrCodeTests.cs | 2 +- Tests.Core.Windows/Tests.Core.Windows.csproj | 20 +++++++++++++++++++ .../WindowsAiUtilitiesTests.cs | 2 +- 6 files changed, 25 insertions(+), 5 deletions(-) rename {Tests => Tests.Core.Windows}/HdrScreenCaptureTests.cs (97%) rename {Tests => Tests.Core.Windows}/ImageChangeDetectorTests.cs (98%) rename {Tests => Tests.Core.Windows}/LanguageTests.cs (98%) rename {Tests => Tests.Core.Windows}/QrCodeTests.cs (89%) rename {Tests => Tests.Core.Windows}/WindowsAiUtilitiesTests.cs (99%) diff --git a/Tests/HdrScreenCaptureTests.cs b/Tests.Core.Windows/HdrScreenCaptureTests.cs similarity index 97% rename from Tests/HdrScreenCaptureTests.cs rename to Tests.Core.Windows/HdrScreenCaptureTests.cs index 6b8e2a7e..92d843b7 100644 --- a/Tests/HdrScreenCaptureTests.cs +++ b/Tests.Core.Windows/HdrScreenCaptureTests.cs @@ -1,7 +1,7 @@ using System.Drawing; using Text_Grab.Utilities.Hdr; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; public class HdrScreenCaptureTests { diff --git a/Tests/ImageChangeDetectorTests.cs b/Tests.Core.Windows/ImageChangeDetectorTests.cs similarity index 98% rename from Tests/ImageChangeDetectorTests.cs rename to Tests.Core.Windows/ImageChangeDetectorTests.cs index 0750dac5..493abd03 100644 --- a/Tests/ImageChangeDetectorTests.cs +++ b/Tests.Core.Windows/ImageChangeDetectorTests.cs @@ -1,7 +1,7 @@ using System.Drawing; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; public class ImageChangeDetectorTests { diff --git a/Tests/LanguageTests.cs b/Tests.Core.Windows/LanguageTests.cs similarity index 98% rename from Tests/LanguageTests.cs rename to Tests.Core.Windows/LanguageTests.cs index 56a53359..609992f1 100644 --- a/Tests/LanguageTests.cs +++ b/Tests.Core.Windows/LanguageTests.cs @@ -2,7 +2,7 @@ using Text_Grab; using Text_Grab.Models; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; public class LanguageTests { diff --git a/Tests/QrCodeTests.cs b/Tests.Core.Windows/QrCodeTests.cs similarity index 89% rename from Tests/QrCodeTests.cs rename to Tests.Core.Windows/QrCodeTests.cs index fe6f78e0..e627603d 100644 --- a/Tests/QrCodeTests.cs +++ b/Tests.Core.Windows/QrCodeTests.cs @@ -1,7 +1,7 @@ using Text_Grab.Utilities; using ZXing.QrCode.Internal; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; public class QrCodeTests { diff --git a/Tests.Core.Windows/Tests.Core.Windows.csproj b/Tests.Core.Windows/Tests.Core.Windows.csproj index e1fee099..aab51b73 100644 --- a/Tests.Core.Windows/Tests.Core.Windows.csproj +++ b/Tests.Core.Windows/Tests.Core.Windows.csproj @@ -27,4 +27,24 @@ + + + + PreserveNewest + + + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/Tests/WindowsAiUtilitiesTests.cs b/Tests.Core.Windows/WindowsAiUtilitiesTests.cs similarity index 99% rename from Tests/WindowsAiUtilitiesTests.cs rename to Tests.Core.Windows/WindowsAiUtilitiesTests.cs index 368b5a39..18f3a30c 100644 --- a/Tests/WindowsAiUtilitiesTests.cs +++ b/Tests.Core.Windows/WindowsAiUtilitiesTests.cs @@ -1,6 +1,6 @@ using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; /// /// Tests for the WindowsAiUtilities.CleanRegexResult method. From 5585199df4eea58a0d351f22b00a7ec272e6ec37 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:47:32 -0500 Subject: [PATCH 47/54] 7a: split five test suites across the tier boundary Five suites turned out to be mixed on closer read (invariant 8's warning about trusting a using-directive census applies to tests too) - each pure half moved under the naming rule from e677b54: the half with more test methods keeps the original name, the smaller half takes a new one. RecognizerExecutorTests (Tests.Core, kept name, 23 methods) vs the GrabTemplateExecutor-backed recognizer-placeholder tests (7 methods) - merged into the existing Tests/GrabTemplateExecutorTests.cs instead of a new class, since GrabTemplateExecutor needs System.Windows.Rect and that file already covers it comprehensively. PatternExecutorTests (Tests.Core, kept name, 10 methods) vs PatternItemCatalogTests (Tests, new, 3 methods) - PatternItemCatalog is the app-side half of the PatternItem split from e677b54. ProtocolUtilitiesTests (Tests.Core, kept name, 9 methods) vs ProtocolHandlerUtilitiesTests (Tests, new, 6 methods) - ProtocolHandlerUtilities is internal and app-side (path validation against the filesystem). BarcodeUtilitiesTests (Tests.Core.Windows, kept name, 3 methods) vs BarcodeUtilitiesImageTests (Tests, new, 1 method) - the one method is [WpfFact]-tagged; Xunit.StaFact cannot be referenced from Tests.Core.Windows, confirmed empirically by loading its net8.0-windows7.0 assembly and checking GetReferencedAssemblies() - it names WindowsBase and System.Windows.Forms, which TierBoundaryTests bans. FilesIoTests (Tests, kept name, 9 methods stay app/WPF-bound) split two ways: IoUtilitiesTests (Tests.Core, new, 3 methods against plain-Core IoUtilities) and FileUtilitiesTests (Tests.Core.Windows, new, 1 method against FileUtilities.GetVisualDocumentFilter). OcrTests is the big one (27 methods vs 15). The pure half kept the OcrTests name and moved to Tests.Core.Windows; the app-coupled half - OcrSourceUtilities calls, WPF BitmapImage construction, direct AppUtilities.TextGrabSettings reads - became Tests/OcrSourceTests.cs. Four methods (OcrComplexTableTestImage, GetTessLanguages, GetTesseractStrongLanguages, GetTesseractGitHubLanguage) were tagged [WpfFact] in the original but never touch a WPF type once OcrUtilities/ TesseractHelper/TesseractGitHubFileDownloader had already moved to Core/Core.Windows in earlier waves; they moved as [Fact]/[Fact(Skip=...)] with no behavior change; [Fact(Skip=...)] tests still compile-check but skip their body, so this only mattered for OcrComplexTableTestImage, which now runs. Two more things came out of reading OcrUtilities in full rather than trusting the method-body census: BuildTextFromOcrLines reads SettingsAccess.Current unconditionally as the first operand of a short-circuited && (so the four BuildTextFromOcrLines_* tests need a resolver even though their assertions don't obviously depend on a setting value), and this project's own namespace ending in ".Windows" makes an unqualified "Windows.Foundation.Rect" resolve against itself and fail to compile - fixed with an explicit "using Windows.Foundation;" and dropping the qualification. Tests.Core.Windows/FakeTextGrabSettings.cs is the resolver those four tests need: a plain ITextGrabSettings POCO seeded from Settings.settings's shipped defaults (RemoveFurigana=true matters concretely - one test's expected output depends on furigana actually being filtered), registered via [ModuleInitializer] the same way the app's SettingsAccessInitializer does. Table-Complex-WordBorders.json moved with OcrComplexTableTestImage since nothing else reads it; Table-Complex.png stayed put (and unused - dead even before this batch, per its own unreferenced private const). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- .../BarcodeUtilitiesTests.cs | 22 +- Tests.Core.Windows/FakeTextGrabSettings.cs | 49 ++ Tests.Core.Windows/FileUtilitiesTests.cs | 20 + Tests.Core.Windows/OcrTests.cs | 540 ++++++++++++++++ .../TextFiles/Table-Complex-WordBorders.json | 0 Tests.Core/IoUtilitiesTests.cs | 46 ++ {Tests => Tests.Core}/PatternExecutorTests.cs | 46 +- .../ProtocolUtilitiesTests.cs | 85 +-- .../RecognizerExecutorTests.cs | 80 +-- Tests/BarcodeUtilitiesImageTests.cs | 26 + Tests/FilesIoTests.cs | 47 +- Tests/GrabTemplateExecutorTests.cs | 72 +++ Tests/{OcrTests.cs => OcrSourceTests.cs} | 585 +----------------- Tests/PatternItemCatalogTests.cs | 47 ++ Tests/ProtocolHandlerUtilitiesTests.cs | 87 +++ Tests/Tests.csproj | 6 - 16 files changed, 941 insertions(+), 817 deletions(-) rename {Tests => Tests.Core.Windows}/BarcodeUtilitiesTests.cs (81%) create mode 100644 Tests.Core.Windows/FakeTextGrabSettings.cs create mode 100644 Tests.Core.Windows/FileUtilitiesTests.cs create mode 100644 Tests.Core.Windows/OcrTests.cs rename {Tests => Tests.Core.Windows}/TextFiles/Table-Complex-WordBorders.json (100%) create mode 100644 Tests.Core/IoUtilitiesTests.cs rename {Tests => Tests.Core}/PatternExecutorTests.cs (71%) rename {Tests => Tests.Core}/ProtocolUtilitiesTests.cs (58%) rename {Tests => Tests.Core}/RecognizerExecutorTests.cs (73%) create mode 100644 Tests/BarcodeUtilitiesImageTests.cs rename Tests/{OcrTests.cs => OcrSourceTests.cs} (50%) create mode 100644 Tests/PatternItemCatalogTests.cs create mode 100644 Tests/ProtocolHandlerUtilitiesTests.cs diff --git a/Tests/BarcodeUtilitiesTests.cs b/Tests.Core.Windows/BarcodeUtilitiesTests.cs similarity index 81% rename from Tests/BarcodeUtilitiesTests.cs rename to Tests.Core.Windows/BarcodeUtilitiesTests.cs index dcdda410..3fe5f20d 100644 --- a/Tests/BarcodeUtilitiesTests.cs +++ b/Tests.Core.Windows/BarcodeUtilitiesTests.cs @@ -5,12 +5,14 @@ using Text_Grab; using Text_Grab.Models; using Text_Grab.Utilities; -using UnitsNet; using Windows.Storage.Streams; -using static System.Net.Mime.MediaTypeNames; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; +// Headless half of the original Tests/BarcodeUtilitiesTests.cs (batch 7a). ReadTestSingleQRCode +// stayed behind as Tests/BarcodeUtilitiesImageTests.cs: it is [WpfFact]-tagged, and Xunit.StaFact +// cannot be referenced here (it pulls in WindowsBase, which TierBoundaryTests bans). This half has +// 3 methods against that one's 1, so it kept the original name. public class BarcodeUtilitiesTests { [Fact] @@ -47,20 +49,6 @@ public void TryToReadBarcodes_WithTwoQrCodes_ReturnsTwoResults() Assert.Contains(results, r => r.RawOutput == "https://example.org"); } - [WpfFact] - public void ReadTestSingleQRCode() - { - string expectedOutput = "This is a test of the QR Code system"; - string testFilePath = FileUtilities.GetPathToLocalFile(@".\Images\QrCodeTestImage.png"); - - Bitmap testBmp = new(testFilePath); - - List result = BarcodeUtilities.TryToReadBarcodes(testBmp); - - Assert.Single(result); - Assert.Equal(expectedOutput, result[0].RawOutput); - } - [Fact] public async Task GetBitmapFromIRandomAccessStream_ReturnsBitmapIndependentOfSourceStream() { diff --git a/Tests.Core.Windows/FakeTextGrabSettings.cs b/Tests.Core.Windows/FakeTextGrabSettings.cs new file mode 100644 index 00000000..6e4f8a81 --- /dev/null +++ b/Tests.Core.Windows/FakeTextGrabSettings.cs @@ -0,0 +1,49 @@ +using System.Runtime.CompilerServices; +using Text_Grab.Interfaces; +using Text_Grab.Services; + +namespace Text_Grab.Tests.Core.Windows; + +/// +/// Registers a resolver for this test host. Tests.Core.Windows has +/// no app assembly to supply one via a [ModuleInitializer] the way Tests does (see +/// SettingsAccess.Current's remarks), so any moved test whose production code path reads settings +/// - here, a handful of OcrTests methods that call into OcrUtilities.BuildTextFromOcrLines, which +/// reads ParagraphDetection/RemoveFurigana/CorrectErrors/CorrectToLatin internally - would throw +/// InvalidOperationException without one installed. +/// +/// Every default below is copied from Text-Grab/Properties/Settings.settings's "(Default)" +/// profile, so a moved test that depends on a default value (e.g. RemoveFurigana=true dropping +/// furigana words) sees exactly what it saw running inside the app-hosted Tests project. +/// +internal static class TestSettingsInitializer +{ + [ModuleInitializer] + internal static void Register() => SettingsAccess.SetResolver(() => new FakeTextGrabSettings()); +} + +/// Minimal ITextGrabSettings double seeded with Settings.settings's shipped defaults. +internal sealed class FakeTextGrabSettings : ITextGrabSettings +{ + public bool CorrectErrors { get; set; } = true; + public bool CorrectToLatin { get; set; } = true; + public bool OverrideAiArchCheck { get; set; } + public bool ParagraphDetection { get; set; } = true; + public bool RemoveFurigana { get; set; } = true; + public bool TryToReadBarcodes { get; set; } = true; + public bool HdrCaptureCorrection { get; set; } + public bool HdrBorderlessGranted { get; set; } + public bool UiAutomationEnabled { get; set; } + public bool WindowsAiDescriptionEnabled { get; set; } + public bool UiAutomationFallbackToOcr { get; set; } = true; + public bool UseTesseract { get; set; } + public string TesseractPath { get; set; } = string.Empty; + public string LastUsedLang { get; set; } = string.Empty; + public int TtsSpeakWordLimit { get; set; } = 100; + public string TtsVoiceName { get; set; } = string.Empty; + public double TtsSpeakingRate { get; set; } = 1; + public string AudioTranscriptionModel { get; set; } = "BaseMultilingual"; + public bool EnableFileBackedManagedSettings { get; set; } + + public void Save() { } +} diff --git a/Tests.Core.Windows/FileUtilitiesTests.cs b/Tests.Core.Windows/FileUtilitiesTests.cs new file mode 100644 index 00000000..eed5806c --- /dev/null +++ b/Tests.Core.Windows/FileUtilitiesTests.cs @@ -0,0 +1,20 @@ +using Text_Grab.Utilities; + +namespace Text_Grab.Tests.Core.Windows; + +// Pure half of the original Tests/FilesIoTests.cs (batch 7a): FileUtilities.GetVisualDocumentFilter +// is Core.Windows-only and needs no app type. The rest of that file needed WPF or app-side members +// and kept the FilesIoTests name in Tests; the IoUtilities-only tests moved separately to +// Tests.Core/IoUtilitiesTests.cs. +public class FileUtilitiesTests +{ + [Fact] + public void GetVisualDocumentFilter_IncludesPdfSupport() + { + string filter = FileUtilities.GetVisualDocumentFilter(); + + Assert.Contains("Image and PDF files|", filter); + Assert.Contains("PDF files|*.pdf", filter); + Assert.Contains("Image files|", filter); + } +} diff --git a/Tests.Core.Windows/OcrTests.cs b/Tests.Core.Windows/OcrTests.cs new file mode 100644 index 00000000..629299d9 --- /dev/null +++ b/Tests.Core.Windows/OcrTests.cs @@ -0,0 +1,540 @@ +// This is the headless split of the original Tests/OcrTests.cs (batch 7a). The other half - +// live OCR-engine calls through OcrSourceUtilities, anything touching BitmapImage, and anything +// reading AppUtilities.TextGrabSettings directly - stayed behind as Tests/OcrSourceTests.cs. +// This half outnumbers that one (27 methods vs 15), so it kept the original name. Four methods +// (OcrComplexTableTestImage, GetTessLanguages, GetTesseractStrongLanguages, +// GetTesseractGitHubLanguage) were tagged [WpfFact] in the original file but never touch a WPF +// type - Xunit.StaFact cannot be referenced here (it pulls in WindowsBase, which +// TierBoundaryTests bans), so they moved as plain [Fact]/[Fact(Skip=...)] with no behavior +// change. +using System.Drawing; +using System.IO; +using System.Text; +using System.Text.Json; +using Text_Grab.Interfaces; +using Text_Grab.Models; +using Text_Grab.Utilities; +using Windows.Foundation; + +namespace Text_Grab.Tests.Core.Windows; + +public class OcrTests +{ + private const string ComplexWordBorders = @".\TextFiles\Table-Complex-WordBorders.json"; + private const string ComplexTableResult = @"DESCRIPTION YEAR TO DATE ACTUAL ANNUAL BUDGET BALANCE % BUDGET REMAINING +CORPORATE INCOME (1) $138,553 $358,100 $219,547 61 % +FOUNDATION INCOME 432,275 824,700 392,425 48% +GOVERNMENT INCOME 375,375 833,825 458,450 55% +PUBLICATIONS INCOME 1,341 3,000 1,659 55% +INTEREST INCOME (2) 26,767 39,000 12,233 31% +INVESTMENT GAIN (3) 50,472 0 N/A N/A +MISCELLANEOUS INCOME 1,650 6,995 5,345 76% +TOTAL REVENUE 1,026,433 2,065,620 1,089,659 53% +SALARIES & WAGES 355,633 603,840 248,207 41% +FRINGE BENEFITS 63,182 120,120 56,938 47% +OFFICE RENT 83,131 132,000 48,869 37% +EQUIPMENT RENTAL & MAINTENANCE 15,364 19,900 4,536 23% +SUPPLIES 8,051 10,200 2,149 21% +TELEPHONE AND POSTAGE 15,088 24,100 9,012 37% +INSURANCE 6,149 5,500 (649) (12)% +REGISTRATION & LICENSES 415 760 345 45% +DEPRECIATION 8,482 17,000 8,518 50% +BANK CHARGES 344 670 326 49% +AUDIT FEES 19,000 19,000 0 0% +BOARD MEETINGS 12,541 20,000 7,459 37% +TRAVEL 6,910 20,000 13,090 65% +LODGING & PERDIEM 15,623 20,000 4,377 22% +SEMINARS & MEETINGS 3,442 8,700 5,258 60% +PROFESSIONAL FESS 5,050 16,000 10,950 68% +PRINTING & PUBLICATIONS 25,576 25,000 (576) (2) % +MATERIALS,SUBS,DUES & TRAININGS 4,445 6,800 2,355 35% +LOCAL STAFF DEVELOPMENT 0 7,500 7,500 100% +STIPENDS 8,250 9,750 1,500 15% +SUBTOTAL 656,675 1,086,840 430,165 40% +TRANSFER PAYMENTS TO SUBRECIPIENTS 360,009 978,780 618,771 63% +TOTAL EXPENDITURES 1,016,684 2,065,620 1,048,936 51% +REVENUES OVERY(UNDER) EXPENDITURES $9,749 $0 $9,749 N/A"; + + [Theory] + [InlineData(10, 10, 25, 10, true)] // bounding-box gap = 5 + [InlineData(10, 10, 26, 10, false)] // threshold boundary: gap = 6 + [InlineData(10, 10, 27, 10, false)] // bounding-box gap = 7 + [InlineData(10, 10, 10, 10, false)] // same visual row + [InlineData(10, 10, 14, 10, false)] // insufficient vertical advance + [InlineData(10, 10, 18, 10, true)] // distinct rows with slight overlap + [InlineData(10, 10, 16, 30, false)] // height ratio = 3 + [InlineData(10, 0, 13, 10, false)] // zero height + public void IsWrappedParagraph_ReturnsExpected( + double currentTop, double currentHeight, + double nextTop, double nextHeight, + bool expected) + { + bool result = OcrUtilities.IsWrappedParagraph(currentTop, currentHeight, nextTop, nextHeight); + Assert.Equal(expected, result); + } + [Fact] + public void GroupWrappedParagraphLines_CombinesWrappedLinesIntoParagraphBlocks() + { + List lines = + [ + new(0, "Static cling is the tendency", new Rect(0, 0, 100, 10)), + new(1, "for light objects to stick.", new Rect(0, 14, 100, 10)), + new(2, "New paragraph.", new Rect(0, 32, 120, 12)), + ]; + + List groups = OcrUtilities.GroupWrappedParagraphLines(lines); + + Assert.Equal(2, groups.Count); + Assert.Equal(0, groups[0].StartingLineNumber); + Assert.Equal("Static cling is the tendency for light objects to stick.", groups[0].SingleLineText); + Assert.Equal($"Static cling is the tendency{Environment.NewLine}for light objects to stick.", groups[0].DisplayText); + Assert.Equal(0, groups[0].BoundingBox.Y); + Assert.Equal(24, groups[0].BoundingBox.Height); + Assert.Equal("New paragraph.", groups[1].SingleLineText); + } + [Fact] + public void GroupWrappedParagraphLines_DoesNotMergeEntriesOnTheSameVisualRow() + { + List lines = + [ + new(0, "Left entry", new Rect(0, 10, 50, 10)), + new(1, "Right entry", new Rect(60, 10, 50, 10)), + ]; + + List groups = OcrUtilities.GroupWrappedParagraphLines(lines); + + Assert.Equal(2, groups.Count); + Assert.All(groups, group => Assert.DoesNotContain(Environment.NewLine, group.DisplayText)); + Assert.All(groups, group => Assert.Equal(10, group.BoundingBox.Height)); + } + [Fact] + public void GroupWrappedParagraphLines_RemovesEmbeddedLineBreaksFromIndividualOcrLines() + { + List lines = + [ + new(0, $"First{Environment.NewLine}line", new Rect(0, 0, 100, 10)), + ]; + + OcrUtilities.GroupedOcrLines group = Assert.Single(OcrUtilities.GroupWrappedParagraphLines(lines)); + + Assert.Equal("First line", group.DisplayText); + Assert.Equal("First line", group.SingleLineText); + } + [Fact] + public async Task OcrComplexTableTestImage() + { + // Given + string resultWordBorders = ComplexWordBorders; + string expectedResult = ComplexTableResult; + string wordBordersJson = await File.ReadAllTextAsync( + FileUtilities.GetPathToLocalFile(resultWordBorders), + TestContext.Current.CancellationToken); + + List wbInfoList = JsonSerializer.Deserialize>(wordBordersJson ?? "[]") + ?? throw new Exception("Failed to deserialize WordBorderInfo list"); + + // When + // 1514 x 1243 image size + Rectangle rectCanvasSize = new() + { + Width = 1514, + Height = 1243, + X = 0, + Y = 0 + }; + + ResultTable resultTable = new(); + resultTable.AnalyzeAsTable(wbInfoList, rectCanvasSize); + StringBuilder stringBuilder = new(); + + ResultTable.GetTextFromTabledWordBorders(stringBuilder, wbInfoList, true); + + // Then + Assert.Equal(expectedResult, stringBuilder.ToString()); + } + [Fact(Skip = "fails GitHub actions")] + public async Task GetTessLanguages() + { + List expected = ["eng", "spa"]; + List actualStrings = await TesseractHelper.TesseractLanguagesAsStrings(); + + if (actualStrings.Count == 0) + return; + + foreach (string tag in expected) + { + Assert.Contains(tag, actualStrings); + } + } + [Fact(Skip = "fails GitHub actions")] + public async Task GetTesseractStrongLanguages() + { + List expectedList = + [ + new TessLang("eng"), + new TessLang("spa"), + ]; + + List actualList = await TesseractHelper.TesseractLanguages(); + + if (actualList.Count == 0) + return; + + foreach (ILanguage tag in expectedList) + { + Assert.Contains(tag.AbbreviatedName, actualList.Select(x => x.AbbreviatedName).ToList()); + } + } + [Fact(Skip = "fails GitHub actions")] + public async Task GetTesseractGitHubLanguage() + { + TesseractGitHubFileDownloader fileDownloader = new(); + + int length = TesseractGitHubFileDownloader.tesseractTrainedDataFileNames.Length; + string languageFileDataName = TesseractGitHubFileDownloader.tesseractTrainedDataFileNames[new Random().Next(length)]; + string tempFilePath = Path.Combine(Path.GetTempPath(), languageFileDataName); + + await fileDownloader.DownloadFileAsync(languageFileDataName, tempFilePath); + + Assert.True(File.Exists(tempFilePath)); + Assert.True(new FileInfo(tempFilePath).Length > 0); + + File.Delete(tempFilePath); + } + [Fact] + public void BuildTextFromOcrLines_FiltersFuriganaForJapanese() + { + // Given a Japanese line where the kanji 黒 is annotated with the small + // furigana くろ rendered directly above it. + FakeOcrLine line = new("くろ黒ごま", new Rect(0, 0, 60, 30)) + { + Words = + [ + // Furigana: short and sitting above the kanji it annotates. + new FakeOcrWord("くろ", new Rect(0, 0, 16, 8)), + // Main text: full-height single characters. + new FakeOcrWord("黒", new Rect(0, 10, 20, 20)), + new FakeOcrWord("ご", new Rect(20, 10, 20, 20)), + new FakeOcrWord("ま", new Rect(40, 10, 20, 20)), + ] + }; + + FakeOcrLinesWords ocrResult = new() { Lines = [line] }; + + // When + string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("ja"), ocrResult); + + // Then the furigana is dropped, leaving only the main text. + Assert.Equal("黒ごま", text); + } + [Fact] + public void FilterFurigana_EmptyList_ReturnsEmpty() + { + List result = OcrUtilities.FilterFurigana([]); + + Assert.Empty(result); + } + [Fact] + public void FilterFurigana_SingleWord_IsKept() + { + List words = [Word("黒", 0, 0, 20, 20)]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["黒"], result.Select(w => w.Text)); + } + [Fact] + public void FilterFurigana_UniformHeights_KeepsAllInOrder() + { + // No word is small relative to the median, so nothing is furigana. + List words = + [ + Word("黒", 0, 0, 20, 20), + Word("ご", 20, 0, 20, 20), + Word("ま", 40, 0, 20, 20), + ]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["黒", "ご", "ま"], result.Select(w => w.Text)); + } + [Fact] + public void FilterFurigana_RemovesSmallWordAboveOverlappingKanji() + { + List words = + [ + Word("くろ", 0, 0, 16, 8), // furigana: short, sitting above + Word("黒", 0, 10, 20, 20), // kanji: taller, below, overlapping + ]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["黒"], result.Select(w => w.Text)); + } + [Fact] + public void FilterFurigana_KeepsSmallWordWhenNotHorizontallyOverlapping() + { + // Small, but nowhere near a kanji horizontally, so it is real text. + List words = + [ + Word("くろ", 100, 0, 16, 8), + Word("黒", 0, 10, 20, 20), + ]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["くろ", "黒"], result.Select(w => w.Text)); + } + [Fact] + public void FilterFurigana_KeepsSmallWordBelowMainText() + { + // Furigana sits above its kanji; a small word BELOW a larger word is + // not furigana and must be kept. + List words = + [ + Word("黒", 0, 0, 20, 20), + Word("くろ", 0, 22, 16, 8), + ]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["黒", "くろ"], result.Select(w => w.Text)); + } + [Fact] + public void FilterFurigana_KeepsSmallWordWhenWordBelowIsNotLarger() + { + // A small word directly above another small word is not furigana: + // furigana requires a larger word (the kanji) beneath it. The two tall + // words only exist to raise the median height. + List words = + [ + Word("く", 0, 0, 8, 8), + Word("ろ", 0, 10, 8, 8), // below + overlapping, but also small + Word("本", 50, 0, 20, 20), + Word("語", 80, 0, 20, 20), + ]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["く", "ろ", "本", "語"], result.Select(w => w.Text)); + } + [Theory] + [InlineData("く", true)] // 1-char ruby is removed + [InlineData("くろ", true)] // 2-char ruby is removed + [InlineData("くろが", false)] // 3+ chars is treated as real text and kept + public void FilterFurigana_OnlyRemovesShortWords(string rubyText, bool removed) + { + List words = + [ + Word(rubyText, 0, 0, 16, 8), + Word("黒", 0, 10, 20, 20), + ]; + + List result = OcrUtilities.FilterFurigana(words); + + string[] expected = removed ? ["黒"] : [rubyText, "黒"]; + Assert.Equal(expected, result.Select(w => w.Text)); + } + [Fact] + public void FilterFurigana_RemovesMultipleFuriganaKeepingMainText() + { + List words = + [ + Word("くろ", 0, 0, 16, 8), + Word("黒", 0, 10, 20, 20), + Word("ごま", 20, 0, 16, 8), + Word("米", 20, 10, 20, 20), + ]; + + List result = OcrUtilities.FilterFurigana(words); + + Assert.Equal(["黒", "米"], result.Select(w => w.Text)); + } + [Fact] + public void BuildTextFromOcrLines_JapaneseWithoutFurigana_IsUnchanged() + { + FakeOcrLine line = new("黒ごま", new Rect(0, 0, 60, 20)) + { + Words = + [ + Word("黒", 0, 0, 20, 20), + Word("ご", 20, 0, 20, 20), + Word("ま", 40, 0, 20, 20), + ] + }; + FakeOcrLinesWords ocrResult = new() { Lines = [line] }; + + string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("ja"), ocrResult); + + Assert.Equal("黒ごま", text); + } + [Fact] + public void BuildTextFromOcrLines_ChineseText_JoinsWithoutSpaces() + { + FakeOcrLine line = new("中文", new Rect(0, 0, 40, 20)) + { + Words = + [ + Word("中", 0, 0, 20, 20), + Word("文", 20, 0, 20, 20), + ] + }; + FakeOcrLinesWords ocrResult = new() { Lines = [line] }; + + string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("zh-Hans"), ocrResult); + + Assert.Equal("中文", text); + } + [Fact] + public void BuildTextFromOcrLines_FiltersRubyTextForChinese() + { + // The same small-ruby heuristic also runs for Chinese, another + // non-space-joining language (e.g. bopomofo above a character). + FakeOcrLine line = new("ㄓ中文", new Rect(0, 0, 40, 30)) + { + Words = + [ + Word("ㄓ", 0, 0, 8, 8), + Word("中", 0, 10, 20, 20), + Word("文", 20, 10, 20, 20), + ] + }; + FakeOcrLinesWords ocrResult = new() { Lines = [line] }; + + string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("zh-Hans"), ocrResult); + + Assert.Equal("中文", text); + } + [Fact] + public void OrderLinesForReadingFlow_SortsRowsTopToBottomAndLeftToRight() + { + // Mimics the Windows OCR engine returning furigana ruby lines and a + // trailing fragment out of reading order (as seen with Ja-Lang-Image.png). + // Row 1 (y~0): furigana くろ + main-line reading, emitted out of x-order. + // Row 2 (y~30): the main text line. + FakeOcrLine furiganaRight = new("しつ", new Rect(200, 0, 20, 8)); + FakeOcrLine furiganaLeft = new("くろ", new Rect(0, 0, 20, 8)); + FakeOcrLine mainLine = new("黒ごま質", new Rect(0, 30, 240, 20)); + + // Engine order is scrambled: right furigana, main line, then left furigana. + FakeOcrLinesWords ocrResult = new() + { + Lines = [furiganaRight, mainLine, furiganaLeft] + }; + + IReadOnlyList ordered = OcrUtilities.OrderLinesForReadingFlow(ocrResult.Lines); + + Assert.Equal(["くろ", "しつ", "黒ごま質"], ordered.Select(l => l.Text)); + } + [Fact] + public void OrderLinesForReadingFlow_KeepsSeparateRowsInVerticalOrder() + { + // Two furigana rows and two main-text rows interleaved and shuffled must + // come back strictly top-to-bottom. + FakeOcrLine ruby2 = new("かみ", new Rect(0, 100, 20, 8)); + FakeOcrLine main2 = new("髪", new Rect(0, 130, 40, 20)); + FakeOcrLine ruby1 = new("くろ", new Rect(0, 0, 20, 8)); + FakeOcrLine main1 = new("黒", new Rect(0, 30, 40, 20)); + + FakeOcrLinesWords ocrResult = new() { Lines = [main2, ruby1, main1, ruby2] }; + + IReadOnlyList ordered = OcrUtilities.OrderLinesForReadingFlow(ocrResult.Lines); + + Assert.Equal(["くろ", "黒", "かみ", "髪"], ordered.Select(l => l.Text)); + } + [Fact] + public void FilterFuriganaLines_RemovesShortLineAboveTallerOverlappingLine() + { + // A short furigana line sitting just above a taller kanji line that it + // overlaps horizontally is dropped. + FakeOcrLine furigana = new("くろ", new Rect(0, 0, 40, 8)); + FakeOcrLine mainLine = new("黒ごま", new Rect(0, 10, 120, 20)); + + FakeOcrLinesWords ocrResult = new() { Lines = [furigana, mainLine] }; + + IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); + + Assert.Equal(["黒ごま"], result.Select(l => l.Text)); + } + [Fact] + public void FilterFuriganaLines_KeepsTwoBodyLinesOfSimilarHeight() + { + // Two normal body lines stacked vertically: neither is much shorter than + // the other, so nothing is treated as furigana. + FakeOcrLine top = new("黒ごまは体に", new Rect(0, 0, 200, 20)); + FakeOcrLine bottom = new("たくさんあります", new Rect(0, 26, 200, 20)); + + FakeOcrLinesWords ocrResult = new() { Lines = [top, bottom] }; + + IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); + + Assert.Equal(["黒ごまは体に", "たくさんあります"], result.Select(l => l.Text)); + } + [Fact] + public void FilterFuriganaLines_KeepsShortLineNotHorizontallyOverlappingAnyKanji() + { + // A short line off to the side (no taller line beneath it) is real text. + FakeOcrLine shortSide = new("注", new Rect(300, 0, 20, 8)); + FakeOcrLine mainLine = new("黒ごま", new Rect(0, 10, 120, 20)); + + FakeOcrLinesWords ocrResult = new() { Lines = [shortSide, mainLine] }; + + IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); + + Assert.Equal(["注", "黒ごま"], result.Select(l => l.Text)); + } + [Fact] + public void FilterFuriganaLines_KeepsShortLineWhenGapIsTooLarge() + { + // Short line far above a taller line is a separate heading/body line, not + // a hugging ruby annotation, so it is kept. + FakeOcrLine shortHeading = new("メモ", new Rect(0, 0, 40, 8)); + FakeOcrLine mainLine = new("黒ごま", new Rect(0, 60, 120, 20)); + + FakeOcrLinesWords ocrResult = new() { Lines = [shortHeading, mainLine] }; + + IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); + + Assert.Equal(["メモ", "黒ごま"], result.Select(l => l.Text)); + } + + private static FakeOcrWord Word(string text, double x, double y, double width, double height) + => new(text, new Rect(x, y, width, height)); + + private sealed class FakeOcrLinesWords : IOcrLinesWords + { + public string Text { get; set; } = string.Empty; + + public IOcrLine[] Lines { get; set; } = []; + + public float Angle { get; set; } + } + + private sealed class FakeOcrLine : IOcrLine + { + public FakeOcrLine(string text, Rect boundingBox) + { + Text = text; + BoundingBox = boundingBox; + } + + public string Text { get; set; } + + public IOcrWord[] Words { get; set; } = []; + + public Rect BoundingBox { get; set; } + } + + private sealed class FakeOcrWord : IOcrWord + { + public FakeOcrWord(string text, Rect boundingBox) + { + Text = text; + BoundingBox = boundingBox; + } + + public string Text { get; set; } + + public Rect BoundingBox { get; set; } + } +} diff --git a/Tests/TextFiles/Table-Complex-WordBorders.json b/Tests.Core.Windows/TextFiles/Table-Complex-WordBorders.json similarity index 100% rename from Tests/TextFiles/Table-Complex-WordBorders.json rename to Tests.Core.Windows/TextFiles/Table-Complex-WordBorders.json diff --git a/Tests.Core/IoUtilitiesTests.cs b/Tests.Core/IoUtilitiesTests.cs new file mode 100644 index 00000000..86d7fadf --- /dev/null +++ b/Tests.Core/IoUtilitiesTests.cs @@ -0,0 +1,46 @@ +using Text_Grab; +using Text_Grab.Models; +using Text_Grab.Utilities; + +namespace Text_Grab.Tests.Core; + +// Pure half of the original Tests/FilesIoTests.cs (batch 7a): these three methods only touch +// Text_Grab.Utilities.IoUtilities, which is plain Core. The rest of that file needed WPF +// ([WpfFact]/[WpfTheory]) or app-side FileUtilities/OpenDocumentFilterUtilities/App members and +// kept the FilesIoTests name in Tests. FileUtilities.GetVisualDocumentFilter, the other pure +// method in that file, is Core.Windows-only and moved separately to +// Tests.Core.Windows/FileUtilitiesTests.cs. +public class IoUtilitiesTests +{ + [Theory] + [InlineData(@"C:\Temp\sheet.csv", EtwEditorMode.Spreadsheet)] + [InlineData(@"C:\Temp\sheet.TSV", EtwEditorMode.Spreadsheet)] + [InlineData(@"C:\Temp\sheet.tab", EtwEditorMode.Spreadsheet)] + [InlineData(@"C:\Temp\notes.md", EtwEditorMode.Markdown)] + [InlineData(@"C:\Temp\notes.markdown", EtwEditorMode.Markdown)] + [InlineData(@"C:\Temp\notes.txt", EtwEditorMode.Text)] + [InlineData(@"C:\Temp\data.json", EtwEditorMode.Text)] + public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode expectedMode) + { + Assert.Equal(expectedMode, IoUtilities.GetEditorModeForPath(path)); + } + + [Theory] + [InlineData(@"C:\Temp\scan.png", OpenContentKind.Image)] + [InlineData(@"C:\Temp\scan.PDF", OpenContentKind.PdfDocument)] + [InlineData(@"C:\Temp\notes.txt", OpenContentKind.TextFile)] + public void GetOpenContentKindForPath_ClassifiesVisualDocumentsAndText(string path, OpenContentKind expectedKind) + { + Assert.Equal(expectedKind, IoUtilities.GetOpenContentKindForPath(path)); + } + + [Theory] + [InlineData(".png", true)] + [InlineData(".PDF", true)] + [InlineData(".txt", false)] + [InlineData("", false)] + public void IsVisualDocumentFileExtension_RecognizesImagesAndPdf(string extension, bool expected) + { + Assert.Equal(expected, IoUtilities.IsVisualDocumentFileExtension(extension)); + } +} diff --git a/Tests/PatternExecutorTests.cs b/Tests.Core/PatternExecutorTests.cs similarity index 71% rename from Tests/PatternExecutorTests.cs rename to Tests.Core/PatternExecutorTests.cs index 36a4e20e..2b13a7b9 100644 --- a/Tests/PatternExecutorTests.cs +++ b/Tests.Core/PatternExecutorTests.cs @@ -1,8 +1,13 @@ using Text_Grab.Models; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; +// Pure half of the original Tests/PatternExecutorTests.cs (batch 7a): PatternExecutor and +// StoredRegex-backed PatternItem construction are Core-only. The three PatternItemCatalog +// tests stayed behind as Tests/PatternItemCatalogTests.cs - PatternItemCatalog.GetAll()/ +// GetByName() are app-side (Text-Grab/Models/PatternItemCatalog.cs, per e677b54). This half +// has 10 methods against that one's 3, so it kept the original name. public class PatternExecutorTests { // A deterministic saved-regex item that does not depend on the machine's saved patterns. @@ -12,45 +17,6 @@ private static PatternItem SavedEmail() => private static PatternItem RecognizerByName(string name) => new(BuiltInRecognizer.GetByName(name) ?? throw new InvalidOperationException($"missing recognizer {name}")); - // ── PatternItem catalog ─────────────────────────────────────────────────── - - [Fact] - public void GetAll_ListsSavedRegexesBeforeRecognizers() - { - IReadOnlyList all = PatternItemCatalog.GetAll(); - - int firstRecognizer = -1; - int lastSaved = -1; - for (int i = 0; i < all.Count; i++) - { - if (all[i].Kind == PatternKind.Recognizer && firstRecognizer < 0) - firstRecognizer = i; - if (all[i].Kind == PatternKind.SavedRegex) - lastSaved = i; - } - - Assert.True(firstRecognizer >= 0, "expected at least one recognizer item"); - Assert.True(lastSaved < firstRecognizer, "all saved regexes should precede recognizers"); - } - - [Fact] - public void GetAll_IncludesEveryRecognizerWithSmartGroup() - { - List recognizers = [.. PatternItemCatalog.GetAll().Where(p => p.Kind == PatternKind.Recognizer)]; - - Assert.Equal(BuiltInRecognizer.GetAll().Count, recognizers.Count); - Assert.All(recognizers, p => Assert.Equal(PatternItem.SmartGroup, p.GroupLabel)); - } - - [Fact] - public void GetByName_FindsRecognizer_CaseInsensitive() - { - PatternItem? email = PatternItemCatalog.GetByName("EMAIL"); - - Assert.NotNull(email); - Assert.Equal(PatternKind.Recognizer, email!.Kind); - } - // ── PatternExecutor – recognizer-backed ─────────────────────────────────── [Fact] diff --git a/Tests/ProtocolUtilitiesTests.cs b/Tests.Core/ProtocolUtilitiesTests.cs similarity index 58% rename from Tests/ProtocolUtilitiesTests.cs rename to Tests.Core/ProtocolUtilitiesTests.cs index c6f4945f..52a6ce9f 100644 --- a/Tests/ProtocolUtilitiesTests.cs +++ b/Tests.Core/ProtocolUtilitiesTests.cs @@ -1,9 +1,13 @@ using System; -using System.IO; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; +// Pure half of the original Tests/ProtocolUtilitiesTests.cs (batch 7a): IsProtocolUri and +// TryParseProtocolUri live on Text-Grab.Core's ProtocolUtilities. The +// TryGetSafeProtocolFilePath tests stayed behind as Tests/ProtocolHandlerUtilitiesTests.cs - +// ProtocolHandlerUtilities is app-side (Text-Grab/Utilities/ProtocolHandlerUtilities.cs). This +// half has 9 methods against that one's 6, so it kept the original name. public class ProtocolUtilitiesTests { [Theory] @@ -116,81 +120,4 @@ public void TryParseProtocolUri_IgnoresMalformedQueryPairs() Assert.Single(parameters); Assert.Equal(@"C:\a.png", parameters["path"]); } - - // ── TryGetSafeProtocolFilePath ──────────────────────────────────────────── - - [Fact] - public void TryGetSafeProtocolFilePath_AcceptsImageInTempFolder() - { - string tempImage = Path.Combine(Path.GetTempPath(), $"text-grab-test-{Guid.NewGuid():N}.png"); - File.WriteAllBytes(tempImage, [0]); - try - { - bool safe = ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(tempImage, out string fullPath); - - Assert.True(safe); - Assert.Equal(Path.GetFullPath(tempImage), fullPath); - } - finally - { - File.Delete(tempImage); - } - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData(@"\\server\share\image.png")] // UNC: would trigger an SMB credential leak - [InlineData("//server/share/image.png")] // forward-slash UNC - [InlineData(@"\\?\C:\Windows\image.png")] // extended-length device path - [InlineData(@"\\.\PhysicalDrive0")] // device namespace - public void TryGetSafeProtocolFilePath_RejectsUncDeviceAndEmptyPaths(string? path) - { - Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(path, out _)); - } - - [Fact] - public void TryGetSafeProtocolFilePath_RejectsPathOutsideAllowedRoots() - { - // The Windows folder is never an allowed root; rejection happens before any - // existence check, so the file need not exist. - string outside = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Windows), - $"text-grab-{Guid.NewGuid():N}.png"); - - Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(outside, out _)); - } - - [Fact] - public void TryGetSafeProtocolFilePath_RejectsTraversalEscapingAllowedRoot() - { - // Starts inside Temp but climbs out to the Windows folder. - string traversal = Path.Combine(Path.GetTempPath(), "..", "..", "..", "Windows", "image.png"); - - Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(traversal, out _)); - } - - [Fact] - public void TryGetSafeProtocolFilePath_RejectsNonImageExtensionInAllowedRoot() - { - string tempText = Path.Combine(Path.GetTempPath(), $"text-grab-test-{Guid.NewGuid():N}.txt"); - File.WriteAllText(tempText, "hello"); - try - { - Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(tempText, out _)); - } - finally - { - File.Delete(tempText); - } - } - - [Fact] - public void TryGetSafeProtocolFilePath_RejectsNonexistentImageInAllowedRoot() - { - string missing = Path.Combine(Path.GetTempPath(), $"text-grab-missing-{Guid.NewGuid():N}.png"); - - Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(missing, out _)); - } } diff --git a/Tests/RecognizerExecutorTests.cs b/Tests.Core/RecognizerExecutorTests.cs similarity index 73% rename from Tests/RecognizerExecutorTests.cs rename to Tests.Core/RecognizerExecutorTests.cs index 3de3c575..5672d68b 100644 --- a/Tests/RecognizerExecutorTests.cs +++ b/Tests.Core/RecognizerExecutorTests.cs @@ -2,8 +2,13 @@ using Text_Grab.Models; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core; +// Pure half of the original Tests/RecognizerExecutorTests.cs (batch 7a): RecognizerExecutor and +// BuiltInRecognizer are Core-only. The GrabTemplateExecutor-backed tests (recognizer placeholders +// and parsing) moved into the existing Tests/GrabTemplateExecutorTests.cs instead of a new class - +// GrabTemplateExecutor needs System.Windows.Rect and stays app-side, and that file already covers +// it comprehensively. public class RecognizerExecutorTests { private static BuiltInRecognizer Get(string id) => @@ -130,65 +135,6 @@ public void ApplyRecognizer_MatchedText_KeepsOriginalSpan() Assert.Equal("$5", result); } - // ── GrabTemplateExecutor – recognizer placeholders ──────────────────────── - - [Fact] - public void ApplyRecognizerPlaceholders_AllMatches_Substitutes() - { - string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders("Found {r:Number:all}", "1 2 3"); - Assert.Equal("Found 1, 2, 3", result); - } - - [Fact] - public void ApplyRecognizerPlaceholders_TextOutput_UsesMatchedText() - { - string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders("{r:Currency:first:text}", "it costs $5"); - Assert.Equal("$5", result); - } - - [Fact] - public void ApplyRecognizerPlaceholders_UnknownRecognizer_LeavesPlaceholder() - { - string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders("{r:Nope:first}", "anything 5"); - Assert.Equal("{r:Nope:first}", result); - } - - [Fact] - public void ApplyRecognizerPlaceholders_LeavesPatternPlaceholdersUntouched() - { - // Recognizer pass must only resolve {r:...}, never {p:...} - string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders( - "{p:Email:first} {r:Number:first}", "value 5"); - Assert.Equal("{p:Email:first} 5", result); - } - - // ── GrabTemplateExecutor – parsing ──────────────────────────────────────── - - [Fact] - public void ParseRecognizerMatches_ExtractsModeAndOutputKind() - { - List matches = - GrabTemplateExecutor.ParseRecognizerMatchesFromOutputTemplate("{r:Number:all:text}"); - - TemplateRecognizerMatch match = Assert.Single(matches); - Assert.Equal("Number", match.RecognizerName); - Assert.Equal("all", match.MatchMode); - Assert.Equal(RecognizerOutputKind.MatchedText, match.OutputKind); - Assert.Equal(Get("number").Id, match.RecognizerId); - } - - [Fact] - public void ParseRecognizerMatches_WithSeparator_ParsesValueOutputAndSeparator() - { - List matches = - GrabTemplateExecutor.ParseRecognizerMatchesFromOutputTemplate("{r:Number:all:value:; }"); - - TemplateRecognizerMatch match = Assert.Single(matches); - Assert.Equal("all", match.MatchMode); - Assert.Equal("; ", match.Separator); - Assert.Equal(RecognizerOutputKind.ResolvedValue, match.OutputKind); - } - // ── FormatResolvedValue – resolution shapes (guards library coupling) ───── [Fact] @@ -275,18 +221,4 @@ public void GetMatches_DateTime_ResolvesDateRange() Assert.Equal("2026-01-01 → 2026-01-05", match.ResolvedValue); } - - // ── ApplyTextOnlyTemplate – recognizer-only ─────────────────────────────── - - [Fact] - public void ApplyTextOnlyTemplate_RecognizerPlaceholder_Resolves() - { - GrabTemplate template = new("Numbers") - { - OutputTemplate = "Numbers: {r:Number:all}" - }; - - string result = GrabTemplateExecutor.ApplyTextOnlyTemplate(template, "got 1 and 2"); - Assert.Equal("Numbers: 1, 2", result); - } } diff --git a/Tests/BarcodeUtilitiesImageTests.cs b/Tests/BarcodeUtilitiesImageTests.cs new file mode 100644 index 00000000..65bd45f7 --- /dev/null +++ b/Tests/BarcodeUtilitiesImageTests.cs @@ -0,0 +1,26 @@ +using System.Drawing; +using Text_Grab.Models; +using Text_Grab.Utilities; + +namespace Tests; + +// WPF half of the original Tests/BarcodeUtilitiesTests.cs (batch 7a). [WpfFact] needs +// Xunit.StaFact, which cannot be referenced from Tests.Core.Windows (it pulls in WindowsBase, +// which TierBoundaryTests bans), so this one test stayed behind while the rest moved to +// Tests.Core.Windows/BarcodeUtilitiesTests.cs, which kept the original name. +public class BarcodeUtilitiesImageTests +{ + [WpfFact] + public void ReadTestSingleQRCode() + { + string expectedOutput = "This is a test of the QR Code system"; + string testFilePath = FileUtilities.GetPathToLocalFile(@".\Images\QrCodeTestImage.png"); + + Bitmap testBmp = new(testFilePath); + + List result = BarcodeUtilities.TryToReadBarcodes(testBmp); + + Assert.Single(result); + Assert.Equal(expectedOutput, result[0].RawOutput); + } +} diff --git a/Tests/FilesIoTests.cs b/Tests/FilesIoTests.cs index 0e54a134..248e4735 100644 --- a/Tests/FilesIoTests.cs +++ b/Tests/FilesIoTests.cs @@ -7,6 +7,11 @@ namespace Tests; +// App-coupled remainder of the original file (batch 7a). Its IoUtilities-only tests moved to +// Tests.Core/IoUtilitiesTests.cs and its FileUtilities.GetVisualDocumentFilter test moved to +// Tests.Core.Windows/FileUtilitiesTests.cs. What is left here is blocked on WPF ([WpfFact]/ +// [WpfTheory] needing Xunit.StaFact, which cannot be referenced outside Tests) or on the +// app-side OpenDocumentFilterUtilities/App classes, so it kept the original FilesIoTests name. public class FilesIoTests { private const string fontSamplePath = @"Images\font_sample.png"; @@ -97,48 +102,6 @@ public async Task ReadNotExistingImageFileEmpty(FileStorageKind storageKind) Assert.Null(emptyReturn); } - [Theory] - [InlineData(@"C:\Temp\sheet.csv", EtwEditorMode.Spreadsheet)] - [InlineData(@"C:\Temp\sheet.TSV", EtwEditorMode.Spreadsheet)] - [InlineData(@"C:\Temp\sheet.tab", EtwEditorMode.Spreadsheet)] - [InlineData(@"C:\Temp\notes.md", EtwEditorMode.Markdown)] - [InlineData(@"C:\Temp\notes.markdown", EtwEditorMode.Markdown)] - [InlineData(@"C:\Temp\notes.txt", EtwEditorMode.Text)] - [InlineData(@"C:\Temp\data.json", EtwEditorMode.Text)] - public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode expectedMode) - { - Assert.Equal(expectedMode, IoUtilities.GetEditorModeForPath(path)); - } - - [Theory] - [InlineData(@"C:\Temp\scan.png", OpenContentKind.Image)] - [InlineData(@"C:\Temp\scan.PDF", OpenContentKind.PdfDocument)] - [InlineData(@"C:\Temp\notes.txt", OpenContentKind.TextFile)] - public void GetOpenContentKindForPath_ClassifiesVisualDocumentsAndText(string path, OpenContentKind expectedKind) - { - Assert.Equal(expectedKind, IoUtilities.GetOpenContentKindForPath(path)); - } - - [Theory] - [InlineData(".png", true)] - [InlineData(".PDF", true)] - [InlineData(".txt", false)] - [InlineData("", false)] - public void IsVisualDocumentFileExtension_RecognizesImagesAndPdf(string extension, bool expected) - { - Assert.Equal(expected, IoUtilities.IsVisualDocumentFileExtension(extension)); - } - - [Fact] - public void GetVisualDocumentFilter_IncludesPdfSupport() - { - string filter = FileUtilities.GetVisualDocumentFilter(); - - Assert.Contains("Image and PDF files|", filter); - Assert.Contains("PDF files|*.pdf", filter); - Assert.Contains("Image files|", filter); - } - [Fact] public void GetOpenDocumentFilter_IncludesVisualAndTextOptions() { diff --git a/Tests/GrabTemplateExecutorTests.cs b/Tests/GrabTemplateExecutorTests.cs index 0ee8f830..94f1306a 100644 --- a/Tests/GrabTemplateExecutorTests.cs +++ b/Tests/GrabTemplateExecutorTests.cs @@ -545,4 +545,76 @@ public void ValidateOutputTemplate_InvalidMatchMode_ReturnsIssue() Assert.NotEmpty(issues); Assert.Contains(issues, i => i.Contains("invalid_mode")); } + + // ── Recognizer placeholders ──────────────────────────────────────────────── + // Moved from Tests/RecognizerExecutorTests.cs in batch 7a: GrabTemplateExecutor needs + // System.Windows.Rect (Text-Grab/Utilities/GrabTemplateExecutor.cs), so it stays app-side and + // these tests could not follow the rest of RecognizerExecutorTests to Tests.Core. + + [Fact] + public void ApplyRecognizerPlaceholders_AllMatches_Substitutes() + { + string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders("Found {r:Number:all}", "1 2 3"); + Assert.Equal("Found 1, 2, 3", result); + } + + [Fact] + public void ApplyRecognizerPlaceholders_TextOutput_UsesMatchedText() + { + string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders("{r:Currency:first:text}", "it costs $5"); + Assert.Equal("$5", result); + } + + [Fact] + public void ApplyRecognizerPlaceholders_UnknownRecognizer_LeavesPlaceholder() + { + string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders("{r:Nope:first}", "anything 5"); + Assert.Equal("{r:Nope:first}", result); + } + + [Fact] + public void ApplyRecognizerPlaceholders_LeavesPatternPlaceholdersUntouched() + { + // Recognizer pass must only resolve {r:...}, never {p:...} + string result = GrabTemplateExecutor.ApplyRecognizerPlaceholders( + "{p:Email:first} {r:Number:first}", "value 5"); + Assert.Equal("{p:Email:first} 5", result); + } + + [Fact] + public void ParseRecognizerMatches_ExtractsModeAndOutputKind() + { + List matches = + GrabTemplateExecutor.ParseRecognizerMatchesFromOutputTemplate("{r:Number:all:text}"); + + TemplateRecognizerMatch match = Assert.Single(matches); + Assert.Equal("Number", match.RecognizerName); + Assert.Equal("all", match.MatchMode); + Assert.Equal(RecognizerOutputKind.MatchedText, match.OutputKind); + Assert.Equal(BuiltInRecognizer.GetById("number")!.Id, match.RecognizerId); + } + + [Fact] + public void ParseRecognizerMatches_WithSeparator_ParsesValueOutputAndSeparator() + { + List matches = + GrabTemplateExecutor.ParseRecognizerMatchesFromOutputTemplate("{r:Number:all:value:; }"); + + TemplateRecognizerMatch match = Assert.Single(matches); + Assert.Equal("all", match.MatchMode); + Assert.Equal("; ", match.Separator); + Assert.Equal(RecognizerOutputKind.ResolvedValue, match.OutputKind); + } + + [Fact] + public void ApplyTextOnlyTemplate_RecognizerPlaceholder_Resolves() + { + GrabTemplate template = new("Numbers") + { + OutputTemplate = "Numbers: {r:Number:all}" + }; + + string result = GrabTemplateExecutor.ApplyTextOnlyTemplate(template, "got 1 and 2"); + Assert.Equal("Numbers: 1, 2", result); + } } diff --git a/Tests/OcrTests.cs b/Tests/OcrSourceTests.cs similarity index 50% rename from Tests/OcrTests.cs rename to Tests/OcrSourceTests.cs index a96e336e..b48eaf5f 100644 --- a/Tests/OcrTests.cs +++ b/Tests/OcrSourceTests.cs @@ -1,8 +1,11 @@ +// This is the app-coupled split of the original Tests/OcrTests.cs (batch 7a): live OCR-engine +// calls through OcrSourceUtilities, anything constructing a WPF BitmapImage, and anything +// reading AppUtilities.TextGrabSettings directly. The headless majority (27 methods against +// pure Text_Grab.Utilities.OcrUtilities logic) kept the original OcrTests name and moved to +// Tests.Core.Windows/OcrTests.cs; this file has the remaining 15. using System.Drawing; using System.IO; using System.Text; -using System.Text.Json; -using System.Windows; using System.Windows.Media.Imaging; using Text_Grab; using Text_Grab.Interfaces; @@ -13,7 +16,7 @@ namespace Tests; -public class OcrTests +public class OcrSourceTests { private const string fontSamplePath = @".\Images\font_sample.png"; private const string fontSampleResult = @"Times-Roman @@ -56,53 +59,26 @@ October 10 Fall November 11 Fall December 12 Winter"; - private const string ComplexTablePath = @".\Images\Table-Complex.png"; - private const string ComplexWordBorders = @".\TextFiles\Table-Complex-WordBorders.json"; - private const string ComplexTableResult = @"DESCRIPTION YEAR TO DATE ACTUAL ANNUAL BUDGET BALANCE % BUDGET REMAINING -CORPORATE INCOME (1) $138,553 $358,100 $219,547 61 % -FOUNDATION INCOME 432,275 824,700 392,425 48% -GOVERNMENT INCOME 375,375 833,825 458,450 55% -PUBLICATIONS INCOME 1,341 3,000 1,659 55% -INTEREST INCOME (2) 26,767 39,000 12,233 31% -INVESTMENT GAIN (3) 50,472 0 N/A N/A -MISCELLANEOUS INCOME 1,650 6,995 5,345 76% -TOTAL REVENUE 1,026,433 2,065,620 1,089,659 53% -SALARIES & WAGES 355,633 603,840 248,207 41% -FRINGE BENEFITS 63,182 120,120 56,938 47% -OFFICE RENT 83,131 132,000 48,869 37% -EQUIPMENT RENTAL & MAINTENANCE 15,364 19,900 4,536 23% -SUPPLIES 8,051 10,200 2,149 21% -TELEPHONE AND POSTAGE 15,088 24,100 9,012 37% -INSURANCE 6,149 5,500 (649) (12)% -REGISTRATION & LICENSES 415 760 345 45% -DEPRECIATION 8,482 17,000 8,518 50% -BANK CHARGES 344 670 326 49% -AUDIT FEES 19,000 19,000 0 0% -BOARD MEETINGS 12,541 20,000 7,459 37% -TRAVEL 6,910 20,000 13,090 65% -LODGING & PERDIEM 15,623 20,000 4,377 22% -SEMINARS & MEETINGS 3,442 8,700 5,258 60% -PROFESSIONAL FESS 5,050 16,000 10,950 68% -PRINTING & PUBLICATIONS 25,576 25,000 (576) (2) % -MATERIALS,SUBS,DUES & TRAININGS 4,445 6,800 2,355 35% -LOCAL STAFF DEVELOPMENT 0 7,500 7,500 100% -STIPENDS 8,250 9,750 1,500 15% -SUBTOTAL 656,675 1,086,840 430,165 40% -TRANSFER PAYMENTS TO SUBRECIPIENTS 360,009 978,780 618,771 63% -TOTAL EXPENDITURES 1,016,684 2,065,620 1,048,936 51% -REVENUES OVERY(UNDER) EXPENDITURES $9,749 $0 $9,749 N/A"; - - private const string JaTestExpectedResult = @"""くろ からだ しつ -黒ごまは体にいいです。タンバク質やカルシウムが -かみ くろ こうか -たくさんあります。髪を黒くする効果もあります。 -くろ あぶら はだ かみ りようり -黒ごま油は肌や髪に使います。料理にも使います。 -かゆ た からだ -お粥やデサートに入れます。でも、食べすきると体 -た -によくないです。少しすつ食べましよう。 -"""; + private const string jaTestPath = @".\Images\Ja-Lang-Image.png"; + + // The reading-order-corrected OCR output for Ja-Lang-Image.png. Furigana ruby + // lines are still present inline (they are kept per the current line-ordering + // fix), but every line now appears in top-to-bottom / left-to-right reading + // order instead of the scrambled order the Windows OCR engine returns. + // + // JaTestExpectedResult (above) is the aspirational, fully-corrected target: + // furigana grouped per row with full-width spaces AND engine misreads fixed + // (からだ vs からた, こうか vs カ, ...). Reaching it needs more than ordering: + // furigana row grouping plus OCR error correction that recovers dakuten and + // small-kana the engine drops. This constant captures what is achievable today. + private const string JaReadingOrderResult = + "くろからたしつ黒ごまは体にいいです。タンバク質やカルシウムがかみ彡ろカたくさんあります。髪を黒くする効果もあります。くろあぶらはだかみりようり黒ごま油は肌や髪に使います。料理にも使います。かゆたからだお粥やデサ ー トに入れます。でも、食べすき、ると体たによくないです。少しすっ食べましよう。"; + + // With furigana removal enabled, the ruby-reading lines are dropped and only + // the main body text remains (still subject to the engine's own misreads and + // one stray mis-detected fragment "み彡" the geometry heuristic cannot catch). + private const string JaFuriganaRemovedResult = + "黒ごまは体にいいです。タンバク質やカルシウムがみ彡たくさんあります。髪を黒くする効果もあります。黒ごま油は肌や髪に使います。料理にも使います。お粥やデサ ー トに入れます。でも、食べすき、ると体によくないです。少しすっ食べましよう。"; [Theory] [InlineData("en-US", "H3llO")] @@ -134,7 +110,6 @@ public void CleanOutput_CorrectsOnlyLatinCaptureLanguages(string languageTag, st settings.CorrectErrors = originalCorrectErrors; } } - [WpfFact] public async Task OcrFontSampleImage() { @@ -147,7 +122,6 @@ public async Task OcrFontSampleImage() // Then Assert.Equal(fontSampleResult, ocrTextResult); } - [WpfFact] public async Task OcrFontTestImage() { @@ -162,7 +136,6 @@ public async Task OcrFontTestImage() // Then Assert.Equal(expectedResult, ocrTextResult); } - [WpfFact] public async Task AnalyzeTable() { @@ -198,7 +171,6 @@ public async Task AnalyzeTable() Assert.Equal(expectedResult, stringBuilder.ToString()); } - [WpfFact] public async Task ParagraphWrapDetection() { @@ -221,25 +193,6 @@ public async Task ParagraphWrapDetection() AppUtilities.TextGrabSettings.ParagraphDetection = originalParagraphDetection; } } - - [Theory] - [InlineData(10, 10, 25, 10, true)] // bounding-box gap = 5 - [InlineData(10, 10, 26, 10, false)] // threshold boundary: gap = 6 - [InlineData(10, 10, 27, 10, false)] // bounding-box gap = 7 - [InlineData(10, 10, 10, 10, false)] // same visual row - [InlineData(10, 10, 14, 10, false)] // insufficient vertical advance - [InlineData(10, 10, 18, 10, true)] // distinct rows with slight overlap - [InlineData(10, 10, 16, 30, false)] // height ratio = 3 - [InlineData(10, 0, 13, 10, false)] // zero height - public void IsWrappedParagraph_ReturnsExpected( - double currentTop, double currentHeight, - double nextTop, double nextHeight, - bool expected) - { - bool result = OcrUtilities.IsWrappedParagraph(currentTop, currentHeight, nextTop, nextHeight); - Assert.Equal(expected, result); - } - [Fact] public void BuildTextFromOcrLines_UsesParagraphDetectionForWinAi() { @@ -267,7 +220,6 @@ public void BuildTextFromOcrLines_UsesParagraphDetectionForWinAi() AppUtilities.TextGrabSettings.ParagraphDetection = originalParagraphDetection; } } - [Theory] [InlineData(true, true, false, true)] [InlineData(true, true, true, false)] @@ -292,79 +244,6 @@ public void ShouldUseParagraphDetection_RespectsTableMode( AppUtilities.TextGrabSettings.ParagraphDetection = originalParagraphDetection; } } - - [Fact] - public void GroupWrappedParagraphLines_CombinesWrappedLinesIntoParagraphBlocks() - { - List lines = - [ - new(0, "Static cling is the tendency", new Windows.Foundation.Rect(0, 0, 100, 10)), - new(1, "for light objects to stick.", new Windows.Foundation.Rect(0, 14, 100, 10)), - new(2, "New paragraph.", new Windows.Foundation.Rect(0, 32, 120, 12)), - ]; - - List groups = OcrUtilities.GroupWrappedParagraphLines(lines); - - Assert.Equal(2, groups.Count); - Assert.Equal(0, groups[0].StartingLineNumber); - Assert.Equal("Static cling is the tendency for light objects to stick.", groups[0].SingleLineText); - Assert.Equal($"Static cling is the tendency{Environment.NewLine}for light objects to stick.", groups[0].DisplayText); - Assert.Equal(0, groups[0].BoundingBox.Y); - Assert.Equal(24, groups[0].BoundingBox.Height); - Assert.Equal("New paragraph.", groups[1].SingleLineText); - } - - [Fact] - public void GroupWrappedParagraphLines_DoesNotMergeEntriesOnTheSameVisualRow() - { - List lines = - [ - new(0, "Left entry", new Windows.Foundation.Rect(0, 10, 50, 10)), - new(1, "Right entry", new Windows.Foundation.Rect(60, 10, 50, 10)), - ]; - - List groups = OcrUtilities.GroupWrappedParagraphLines(lines); - - Assert.Equal(2, groups.Count); - Assert.All(groups, group => Assert.DoesNotContain(Environment.NewLine, group.DisplayText)); - Assert.All(groups, group => Assert.Equal(10, group.BoundingBox.Height)); - } - - [Fact] - public void GroupWrappedParagraphLines_RemovesEmbeddedLineBreaksFromIndividualOcrLines() - { - List lines = - [ - new(0, $"First{Environment.NewLine}line", new Windows.Foundation.Rect(0, 0, 100, 10)), - ]; - - OcrUtilities.GroupedOcrLines group = Assert.Single(OcrUtilities.GroupWrappedParagraphLines(lines)); - - Assert.Equal("First line", group.DisplayText); - Assert.Equal("First line", group.SingleLineText); - } - - private const string jaTestPath = @".\Images\Ja-Lang-Image.png"; - - // The reading-order-corrected OCR output for Ja-Lang-Image.png. Furigana ruby - // lines are still present inline (they are kept per the current line-ordering - // fix), but every line now appears in top-to-bottom / left-to-right reading - // order instead of the scrambled order the Windows OCR engine returns. - // - // JaTestExpectedResult (above) is the aspirational, fully-corrected target: - // furigana grouped per row with full-width spaces AND engine misreads fixed - // (からだ vs からた, こうか vs カ, ...). Reaching it needs more than ordering: - // furigana row grouping plus OCR error correction that recovers dakuten and - // small-kana the engine drops. This constant captures what is achievable today. - private const string JaReadingOrderResult = - "くろからたしつ黒ごまは体にいいです。タンバク質やカルシウムがかみ彡ろカたくさんあります。髪を黒くする効果もあります。くろあぶらはだかみりようり黒ごま油は肌や髪に使います。料理にも使います。かゆたからだお粥やデサ ー トに入れます。でも、食べすき、ると体たによくないです。少しすっ食べましよう。"; - - // With furigana removal enabled, the ruby-reading lines are dropped and only - // the main body text remains (still subject to the engine's own misreads and - // one stray mis-detected fragment "み彡" the geometry heuristic cannot catch). - private const string JaFuriganaRemovedResult = - "黒ごまは体にいいです。タンバク質やカルシウムがみ彡たくさんあります。髪を黒くする効果もあります。黒ごま油は肌や髪に使います。料理にも使います。お粥やデサ ー トに入れます。でも、食べすき、ると体によくないです。少しすっ食べましよう。"; - [WpfFact] public async Task OcrJapaneseImage_ReadingOrder_KeepsFuriganaWhenDisabled() { @@ -394,7 +273,6 @@ public async Task OcrJapaneseImage_ReadingOrder_KeepsFuriganaWhenDisabled() settings.RemoveFurigana = originalRemoveFurigana; } } - [WpfFact] public async Task OcrJapaneseImage_RemovesFuriganaWhenEnabled() { @@ -422,7 +300,6 @@ public async Task OcrJapaneseImage_RemovesFuriganaWhenEnabled() settings.RemoveFurigana = originalRemoveFurigana; } } - [WpfFact] public async Task InspectJapaneseOcrOutput() { @@ -472,7 +349,6 @@ public async Task InspectJapaneseOcrOutput() System.Diagnostics.Debug.WriteLine(report.ToString()); System.Diagnostics.Debug.WriteLine($"Report written to {outPath}"); } - [WpfFact] public async Task ReadQrCode() { @@ -486,7 +362,6 @@ public async Task ReadQrCode() // Then Assert.Equal(expectedResult, ocrTextResult); } - [WpfFact] public async Task AnalyzeTable2() { @@ -528,41 +403,6 @@ 300 Brown // Then Assert.Equal(expectedResult, stringBuilder.ToString()); } - - [WpfFact] - public async Task OcrComplexTableTestImage() - { - // Given - string resultWordBorders = ComplexWordBorders; - string expectedResult = ComplexTableResult; - string wordBordersJson = await File.ReadAllTextAsync( - FileUtilities.GetPathToLocalFile(resultWordBorders), - TestContext.Current.CancellationToken); - - List wbInfoList = JsonSerializer.Deserialize>(wordBordersJson ?? "[]") - ?? throw new Exception("Failed to deserialize WordBorderInfo list"); - - // When - // 1514 x 1243 image size - Rectangle rectCanvasSize = new() - { - Width = 1514, - Height = 1243, - X = 0, - Y = 0 - }; - - ResultTable resultTable = new(); - resultTable.AnalyzeAsTable(wbInfoList, rectCanvasSize); - StringBuilder stringBuilder = new(); - - ResultTable.GetTextFromTabledWordBorders(stringBuilder, wbInfoList, true); - - // Then - Assert.Equal(expectedResult, stringBuilder.ToString()); - } - - [WpfFact(Skip = "since the hocr is not being used from Tesseract it will not be tested for now")] public async Task TesseractHocr() { @@ -604,7 +444,6 @@ public async Task TesseractHocr() // Then Assert.Equal(hocrFileContents, tesseractOutput.RawOutput); } - [WpfFact] public async Task TesseractFontSample() { @@ -629,280 +468,6 @@ public async Task TesseractFontSample() // Then Assert.Equal(fontSampleResultForTesseract, tesseractOutput.RawOutput); } - - [WpfFact(Skip = "fails GitHub actions")] - public async Task GetTessLanguages() - { - List expected = ["eng", "spa"]; - List actualStrings = await TesseractHelper.TesseractLanguagesAsStrings(); - - if (actualStrings.Count == 0) - return; - - foreach (string tag in expected) - { - Assert.Contains(tag, actualStrings); - } - } - - [WpfFact(Skip = "fails GitHub actions")] - public async Task GetTesseractStrongLanguages() - { - List expectedList = - [ - new TessLang("eng"), - new TessLang("spa"), - ]; - - List actualList = await TesseractHelper.TesseractLanguages(); - - if (actualList.Count == 0) - return; - - foreach (ILanguage tag in expectedList) - { - Assert.Contains(tag.AbbreviatedName, actualList.Select(x => x.AbbreviatedName).ToList()); - } - } - - [WpfFact(Skip = "fails GitHub actions")] - public async Task GetTesseractGitHubLanguage() - { - TesseractGitHubFileDownloader fileDownloader = new(); - - int length = TesseractGitHubFileDownloader.tesseractTrainedDataFileNames.Length; - string languageFileDataName = TesseractGitHubFileDownloader.tesseractTrainedDataFileNames[new Random().Next(length)]; - string tempFilePath = Path.Combine(Path.GetTempPath(), languageFileDataName); - - await fileDownloader.DownloadFileAsync(languageFileDataName, tempFilePath); - - Assert.True(File.Exists(tempFilePath)); - Assert.True(new FileInfo(tempFilePath).Length > 0); - - File.Delete(tempFilePath); - } - - [Fact] - public void BuildTextFromOcrLines_FiltersFuriganaForJapanese() - { - // Given a Japanese line where the kanji 黒 is annotated with the small - // furigana くろ rendered directly above it. - FakeOcrLine line = new("くろ黒ごま", new Windows.Foundation.Rect(0, 0, 60, 30)) - { - Words = - [ - // Furigana: short and sitting above the kanji it annotates. - new FakeOcrWord("くろ", new Windows.Foundation.Rect(0, 0, 16, 8)), - // Main text: full-height single characters. - new FakeOcrWord("黒", new Windows.Foundation.Rect(0, 10, 20, 20)), - new FakeOcrWord("ご", new Windows.Foundation.Rect(20, 10, 20, 20)), - new FakeOcrWord("ま", new Windows.Foundation.Rect(40, 10, 20, 20)), - ] - }; - - FakeOcrLinesWords ocrResult = new() { Lines = [line] }; - - // When - string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("ja"), ocrResult); - - // Then the furigana is dropped, leaving only the main text. - Assert.Equal("黒ごま", text); - } - - // ----- FilterFurigana unit tests (the geometry heuristic) ----- - - [Fact] - public void FilterFurigana_EmptyList_ReturnsEmpty() - { - List result = OcrUtilities.FilterFurigana([]); - - Assert.Empty(result); - } - - [Fact] - public void FilterFurigana_SingleWord_IsKept() - { - List words = [Word("黒", 0, 0, 20, 20)]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["黒"], result.Select(w => w.Text)); - } - - [Fact] - public void FilterFurigana_UniformHeights_KeepsAllInOrder() - { - // No word is small relative to the median, so nothing is furigana. - List words = - [ - Word("黒", 0, 0, 20, 20), - Word("ご", 20, 0, 20, 20), - Word("ま", 40, 0, 20, 20), - ]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["黒", "ご", "ま"], result.Select(w => w.Text)); - } - - [Fact] - public void FilterFurigana_RemovesSmallWordAboveOverlappingKanji() - { - List words = - [ - Word("くろ", 0, 0, 16, 8), // furigana: short, sitting above - Word("黒", 0, 10, 20, 20), // kanji: taller, below, overlapping - ]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["黒"], result.Select(w => w.Text)); - } - - [Fact] - public void FilterFurigana_KeepsSmallWordWhenNotHorizontallyOverlapping() - { - // Small, but nowhere near a kanji horizontally, so it is real text. - List words = - [ - Word("くろ", 100, 0, 16, 8), - Word("黒", 0, 10, 20, 20), - ]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["くろ", "黒"], result.Select(w => w.Text)); - } - - [Fact] - public void FilterFurigana_KeepsSmallWordBelowMainText() - { - // Furigana sits above its kanji; a small word BELOW a larger word is - // not furigana and must be kept. - List words = - [ - Word("黒", 0, 0, 20, 20), - Word("くろ", 0, 22, 16, 8), - ]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["黒", "くろ"], result.Select(w => w.Text)); - } - - [Fact] - public void FilterFurigana_KeepsSmallWordWhenWordBelowIsNotLarger() - { - // A small word directly above another small word is not furigana: - // furigana requires a larger word (the kanji) beneath it. The two tall - // words only exist to raise the median height. - List words = - [ - Word("く", 0, 0, 8, 8), - Word("ろ", 0, 10, 8, 8), // below + overlapping, but also small - Word("本", 50, 0, 20, 20), - Word("語", 80, 0, 20, 20), - ]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["く", "ろ", "本", "語"], result.Select(w => w.Text)); - } - - [Theory] - [InlineData("く", true)] // 1-char ruby is removed - [InlineData("くろ", true)] // 2-char ruby is removed - [InlineData("くろが", false)] // 3+ chars is treated as real text and kept - public void FilterFurigana_OnlyRemovesShortWords(string rubyText, bool removed) - { - List words = - [ - Word(rubyText, 0, 0, 16, 8), - Word("黒", 0, 10, 20, 20), - ]; - - List result = OcrUtilities.FilterFurigana(words); - - string[] expected = removed ? ["黒"] : [rubyText, "黒"]; - Assert.Equal(expected, result.Select(w => w.Text)); - } - - [Fact] - public void FilterFurigana_RemovesMultipleFuriganaKeepingMainText() - { - List words = - [ - Word("くろ", 0, 0, 16, 8), - Word("黒", 0, 10, 20, 20), - Word("ごま", 20, 0, 16, 8), - Word("米", 20, 10, 20, 20), - ]; - - List result = OcrUtilities.FilterFurigana(words); - - Assert.Equal(["黒", "米"], result.Select(w => w.Text)); - } - - // ----- BuildTextFromOcrLines integration (language gating) ----- - - [Fact] - public void BuildTextFromOcrLines_JapaneseWithoutFurigana_IsUnchanged() - { - FakeOcrLine line = new("黒ごま", new Windows.Foundation.Rect(0, 0, 60, 20)) - { - Words = - [ - Word("黒", 0, 0, 20, 20), - Word("ご", 20, 0, 20, 20), - Word("ま", 40, 0, 20, 20), - ] - }; - FakeOcrLinesWords ocrResult = new() { Lines = [line] }; - - string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("ja"), ocrResult); - - Assert.Equal("黒ごま", text); - } - - [Fact] - public void BuildTextFromOcrLines_ChineseText_JoinsWithoutSpaces() - { - FakeOcrLine line = new("中文", new Windows.Foundation.Rect(0, 0, 40, 20)) - { - Words = - [ - Word("中", 0, 0, 20, 20), - Word("文", 20, 0, 20, 20), - ] - }; - FakeOcrLinesWords ocrResult = new() { Lines = [line] }; - - string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("zh-Hans"), ocrResult); - - Assert.Equal("中文", text); - } - - [Fact] - public void BuildTextFromOcrLines_FiltersRubyTextForChinese() - { - // The same small-ruby heuristic also runs for Chinese, another - // non-space-joining language (e.g. bopomofo above a character). - FakeOcrLine line = new("ㄓ中文", new Windows.Foundation.Rect(0, 0, 40, 30)) - { - Words = - [ - Word("ㄓ", 0, 0, 8, 8), - Word("中", 0, 10, 20, 20), - Word("文", 20, 10, 20, 20), - ] - }; - FakeOcrLinesWords ocrResult = new() { Lines = [line] }; - - string text = OcrUtilities.BuildTextFromOcrLines(new GlobalLang("zh-Hans"), ocrResult); - - Assert.Equal("中文", text); - } - [Fact] public void BuildTextFromOcrLines_SpaceJoiningLanguage_DoesNotFilterFurigana() { @@ -938,104 +503,6 @@ public void BuildTextFromOcrLines_SpaceJoiningLanguage_DoesNotFilterFurigana() } } - [Fact] - public void OrderLinesForReadingFlow_SortsRowsTopToBottomAndLeftToRight() - { - // Mimics the Windows OCR engine returning furigana ruby lines and a - // trailing fragment out of reading order (as seen with Ja-Lang-Image.png). - // Row 1 (y~0): furigana くろ + main-line reading, emitted out of x-order. - // Row 2 (y~30): the main text line. - FakeOcrLine furiganaRight = new("しつ", new Windows.Foundation.Rect(200, 0, 20, 8)); - FakeOcrLine furiganaLeft = new("くろ", new Windows.Foundation.Rect(0, 0, 20, 8)); - FakeOcrLine mainLine = new("黒ごま質", new Windows.Foundation.Rect(0, 30, 240, 20)); - - // Engine order is scrambled: right furigana, main line, then left furigana. - FakeOcrLinesWords ocrResult = new() - { - Lines = [furiganaRight, mainLine, furiganaLeft] - }; - - IReadOnlyList ordered = OcrUtilities.OrderLinesForReadingFlow(ocrResult.Lines); - - Assert.Equal(["くろ", "しつ", "黒ごま質"], ordered.Select(l => l.Text)); - } - - [Fact] - public void OrderLinesForReadingFlow_KeepsSeparateRowsInVerticalOrder() - { - // Two furigana rows and two main-text rows interleaved and shuffled must - // come back strictly top-to-bottom. - FakeOcrLine ruby2 = new("かみ", new Windows.Foundation.Rect(0, 100, 20, 8)); - FakeOcrLine main2 = new("髪", new Windows.Foundation.Rect(0, 130, 40, 20)); - FakeOcrLine ruby1 = new("くろ", new Windows.Foundation.Rect(0, 0, 20, 8)); - FakeOcrLine main1 = new("黒", new Windows.Foundation.Rect(0, 30, 40, 20)); - - FakeOcrLinesWords ocrResult = new() { Lines = [main2, ruby1, main1, ruby2] }; - - IReadOnlyList ordered = OcrUtilities.OrderLinesForReadingFlow(ocrResult.Lines); - - Assert.Equal(["くろ", "黒", "かみ", "髪"], ordered.Select(l => l.Text)); - } - - [Fact] - public void FilterFuriganaLines_RemovesShortLineAboveTallerOverlappingLine() - { - // A short furigana line sitting just above a taller kanji line that it - // overlaps horizontally is dropped. - FakeOcrLine furigana = new("くろ", new Windows.Foundation.Rect(0, 0, 40, 8)); - FakeOcrLine mainLine = new("黒ごま", new Windows.Foundation.Rect(0, 10, 120, 20)); - - FakeOcrLinesWords ocrResult = new() { Lines = [furigana, mainLine] }; - - IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); - - Assert.Equal(["黒ごま"], result.Select(l => l.Text)); - } - - [Fact] - public void FilterFuriganaLines_KeepsTwoBodyLinesOfSimilarHeight() - { - // Two normal body lines stacked vertically: neither is much shorter than - // the other, so nothing is treated as furigana. - FakeOcrLine top = new("黒ごまは体に", new Windows.Foundation.Rect(0, 0, 200, 20)); - FakeOcrLine bottom = new("たくさんあります", new Windows.Foundation.Rect(0, 26, 200, 20)); - - FakeOcrLinesWords ocrResult = new() { Lines = [top, bottom] }; - - IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); - - Assert.Equal(["黒ごまは体に", "たくさんあります"], result.Select(l => l.Text)); - } - - [Fact] - public void FilterFuriganaLines_KeepsShortLineNotHorizontallyOverlappingAnyKanji() - { - // A short line off to the side (no taller line beneath it) is real text. - FakeOcrLine shortSide = new("注", new Windows.Foundation.Rect(300, 0, 20, 8)); - FakeOcrLine mainLine = new("黒ごま", new Windows.Foundation.Rect(0, 10, 120, 20)); - - FakeOcrLinesWords ocrResult = new() { Lines = [shortSide, mainLine] }; - - IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); - - Assert.Equal(["注", "黒ごま"], result.Select(l => l.Text)); - } - - [Fact] - public void FilterFuriganaLines_KeepsShortLineWhenGapIsTooLarge() - { - // Short line far above a taller line is a separate heading/body line, not - // a hugging ruby annotation, so it is kept. - FakeOcrLine shortHeading = new("メモ", new Windows.Foundation.Rect(0, 0, 40, 8)); - FakeOcrLine mainLine = new("黒ごま", new Windows.Foundation.Rect(0, 60, 120, 20)); - - FakeOcrLinesWords ocrResult = new() { Lines = [shortHeading, mainLine] }; - - IReadOnlyList result = OcrUtilities.FilterFuriganaLines(ocrResult.Lines); - - Assert.Equal(["メモ", "黒ごま"], result.Select(l => l.Text)); - } - private static FakeOcrWord Word(string text, double x, double y, double width, double height) => new(text, new Windows.Foundation.Rect(x, y, width, height)); diff --git a/Tests/PatternItemCatalogTests.cs b/Tests/PatternItemCatalogTests.cs new file mode 100644 index 00000000..d7cbe8df --- /dev/null +++ b/Tests/PatternItemCatalogTests.cs @@ -0,0 +1,47 @@ +using Text_Grab.Models; +using Text_Grab.Utilities; + +namespace Tests; + +// App-coupled half of the original Tests/PatternExecutorTests.cs (batch 7a): PatternItemCatalog +// (Text-Grab/Models/PatternItemCatalog.cs) reads settings and stays app-side per e677b54, so +// these three tests could not follow PatternExecutorTests to Tests.Core. +public class PatternItemCatalogTests +{ + [Fact] + public void GetAll_ListsSavedRegexesBeforeRecognizers() + { + IReadOnlyList all = PatternItemCatalog.GetAll(); + + int firstRecognizer = -1; + int lastSaved = -1; + for (int i = 0; i < all.Count; i++) + { + if (all[i].Kind == PatternKind.Recognizer && firstRecognizer < 0) + firstRecognizer = i; + if (all[i].Kind == PatternKind.SavedRegex) + lastSaved = i; + } + + Assert.True(firstRecognizer >= 0, "expected at least one recognizer item"); + Assert.True(lastSaved < firstRecognizer, "all saved regexes should precede recognizers"); + } + + [Fact] + public void GetAll_IncludesEveryRecognizerWithSmartGroup() + { + List recognizers = [.. PatternItemCatalog.GetAll().Where(p => p.Kind == PatternKind.Recognizer)]; + + Assert.Equal(BuiltInRecognizer.GetAll().Count, recognizers.Count); + Assert.All(recognizers, p => Assert.Equal(PatternItem.SmartGroup, p.GroupLabel)); + } + + [Fact] + public void GetByName_FindsRecognizer_CaseInsensitive() + { + PatternItem? email = PatternItemCatalog.GetByName("EMAIL"); + + Assert.NotNull(email); + Assert.Equal(PatternKind.Recognizer, email!.Kind); + } +} diff --git a/Tests/ProtocolHandlerUtilitiesTests.cs b/Tests/ProtocolHandlerUtilitiesTests.cs new file mode 100644 index 00000000..3bd75039 --- /dev/null +++ b/Tests/ProtocolHandlerUtilitiesTests.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using Text_Grab.Utilities; + +namespace Tests; + +// App-side half of the original Tests/ProtocolUtilitiesTests.cs (batch 7a): ProtocolHandlerUtilities +// (Text-Grab/Utilities/ProtocolHandlerUtilities.cs) is internal and app-side by design - it +// validates a companion app's path= parameter against the filesystem. The pure IsProtocolUri/ +// TryParseProtocolUri tests moved to Tests.Core/ProtocolUtilitiesTests.cs, which kept the name. +public class ProtocolHandlerUtilitiesTests +{ + [Fact] + public void TryGetSafeProtocolFilePath_AcceptsImageInTempFolder() + { + string tempImage = Path.Combine(Path.GetTempPath(), $"text-grab-test-{Guid.NewGuid():N}.png"); + File.WriteAllBytes(tempImage, [0]); + try + { + bool safe = ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(tempImage, out string fullPath); + + Assert.True(safe); + Assert.Equal(Path.GetFullPath(tempImage), fullPath); + } + finally + { + File.Delete(tempImage); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(@"\\server\share\image.png")] // UNC: would trigger an SMB credential leak + [InlineData("//server/share/image.png")] // forward-slash UNC + [InlineData(@"\\?\C:\Windows\image.png")] // extended-length device path + [InlineData(@"\\.\PhysicalDrive0")] // device namespace + public void TryGetSafeProtocolFilePath_RejectsUncDeviceAndEmptyPaths(string? path) + { + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(path, out _)); + } + + [Fact] + public void TryGetSafeProtocolFilePath_RejectsPathOutsideAllowedRoots() + { + // The Windows folder is never an allowed root; rejection happens before any + // existence check, so the file need not exist. + string outside = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Windows), + $"text-grab-{Guid.NewGuid():N}.png"); + + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(outside, out _)); + } + + [Fact] + public void TryGetSafeProtocolFilePath_RejectsTraversalEscapingAllowedRoot() + { + // Starts inside Temp but climbs out to the Windows folder. + string traversal = Path.Combine(Path.GetTempPath(), "..", "..", "..", "Windows", "image.png"); + + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(traversal, out _)); + } + + [Fact] + public void TryGetSafeProtocolFilePath_RejectsNonImageExtensionInAllowedRoot() + { + string tempText = Path.Combine(Path.GetTempPath(), $"text-grab-test-{Guid.NewGuid():N}.txt"); + File.WriteAllText(tempText, "hello"); + try + { + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(tempText, out _)); + } + finally + { + File.Delete(tempText); + } + } + + [Fact] + public void TryGetSafeProtocolFilePath_RejectsNonexistentImageInAllowedRoot() + { + string missing = Path.Combine(Path.GetTempPath(), $"text-grab-missing-{Guid.NewGuid():N}.png"); + + Assert.False(ProtocolHandlerUtilities.TryGetSafeProtocolFilePath(missing, out _)); + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 95827918..e84dcfea 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -61,10 +61,4 @@ - - - PreserveNewest - - - From fe4fd59087f653c6e9a774d08a785f8ca65ae81b Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:47:42 -0500 Subject: [PATCH 48/54] 7a: delete ScaffoldingSmokeTests, its job is done It was placeholder scaffolding proving Tests.Core could build and run against Text-Grab.Core before any real suite had moved. That's now true many times over. Baseline total drops from 1517 to 1516 - the only legitimate change to the total this batch makes; every other count shift is tests moving between projects. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Tests.Core/ScaffoldingSmokeTests.cs | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 Tests.Core/ScaffoldingSmokeTests.cs diff --git a/Tests.Core/ScaffoldingSmokeTests.cs b/Tests.Core/ScaffoldingSmokeTests.cs deleted file mode 100644 index 840df60c..00000000 --- a/Tests.Core/ScaffoldingSmokeTests.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Text_Grab.Tests.Core; - -public class ScaffoldingSmokeTests -{ - [Fact] - public void TestsCoreProject_RunsAgainstTextGrabCore() - { - Assert.True(true); - } -} From 695671ff43c34a84f7206e59767ef0b8963abbe6 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 10:50:13 -0500 Subject: [PATCH 49/54] 7a follow-up: restore CRLF and attribute spacing Tests.Core/ProtocolUtilitiesTests.cs came out of the split with LF endings; every source file in this repo is CRLF, and a whole-file ending flip is the one kind of damage git diff will not show you. The four [WpfFact] -> [Fact] rewrites in Tests.Core.Windows/OcrTests.cs dropped the blank line that separated each attribute from the method above it. Both are cosmetic. The attribute conversions themselves check out: OcrComplexTableTestImage reads a JSON sidecar, deserializes WordBorderInfo and drives ResultTable over a System.Drawing.Rectangle - no WPF anywhere - and the three Tesseract methods are Skip'd regardless. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Tests.Core.Windows/OcrTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests.Core.Windows/OcrTests.cs b/Tests.Core.Windows/OcrTests.cs index 629299d9..9cf7acd2 100644 --- a/Tests.Core.Windows/OcrTests.cs +++ b/Tests.Core.Windows/OcrTests.cs @@ -120,6 +120,7 @@ public void GroupWrappedParagraphLines_RemovesEmbeddedLineBreaksFromIndividualOc Assert.Equal("First line", group.DisplayText); Assert.Equal("First line", group.SingleLineText); } + [Fact] public async Task OcrComplexTableTestImage() { @@ -152,6 +153,7 @@ public async Task OcrComplexTableTestImage() // Then Assert.Equal(expectedResult, stringBuilder.ToString()); } + [Fact(Skip = "fails GitHub actions")] public async Task GetTessLanguages() { @@ -166,6 +168,7 @@ public async Task GetTessLanguages() Assert.Contains(tag, actualStrings); } } + [Fact(Skip = "fails GitHub actions")] public async Task GetTesseractStrongLanguages() { @@ -185,6 +188,7 @@ public async Task GetTesseractStrongLanguages() Assert.Contains(tag.AbbreviatedName, actualList.Select(x => x.AbbreviatedName).ToList()); } } + [Fact(Skip = "fails GitHub actions")] public async Task GetTesseractGitHubLanguage() { From 6497dd306cef537130221c3369753eedc48ddc15 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 11:02:30 -0500 Subject: [PATCH 50/54] 7b: remove the dead Table-Complex.png content item Tests.csproj carried a `Content Update="Images\Table-Complex.png"` with no matching `Content Include` for it anywhere in the project - the image and the private const that pointed at it were both dead before batch 7a even ran. 7a's own commit message (5585199) already recorded this when it moved the Table-Complex-WordBorders.json companion to Tests.Core.Windows: "Table-Complex.png stayed put (and unused - dead even before this batch, per its own unreferenced private const)". This batch verifies that independently (git log -S confirms no test has referenced the .png since OcrComplexTableTestImage switched to reading the .json word-border fixture instead) and removes both the orphaned csproj item and the 1.4 MB PNG itself. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Tests/Images/Table-Complex.png | Bin 645280 -> 0 bytes Tests/Tests.csproj | 5 ----- 2 files changed, 5 deletions(-) delete mode 100644 Tests/Images/Table-Complex.png diff --git a/Tests/Images/Table-Complex.png b/Tests/Images/Table-Complex.png deleted file mode 100644 index 116e02b6cde5d8e97e2f02b7db3b0e7c22c37d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 645280 zcmV)GK)%0;P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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�v~TQ1N#gUZD~o>Vv&pp_B&#j_OB- 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

0+TgeiDt4Gf2($X!FID9pTO5`vMb2A?FI!qKai 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

O~>%H4@$lq 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�aV73fKbk28~i|gs!N<46Zzt9g^do0LImv2?onM> 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 NTS{koCg~S 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 IqKwy^W2!kI)$-J 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

)! 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 JPDHLkV1m%XrBMI? diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index e84dcfea..d651ba05 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -55,10 +55,5 @@ - - - PreserveNewest - - From 3f4b222dcfb64304eee1ecaf0439e0a1434fd94f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 11:03:06 -0500 Subject: [PATCH 51/54] 7b: move GrabFrameFileUtilities and FileAssociationUtilities to Core.Windows Section 7 had both files marked "unblocked by a8591aa, never audited past that blocker" - HistoryInfo (their root pin to the app) moved to Core.Windows in that commit, but nobody had re-read either file since to check for a second blocker. This batch is that audit, and the answer is: no second blocker, both move clean. GrabFrameFileUtilities.cs needed zero code changes - it already traffics entirely in System.Drawing.Bitmap (Core.Windows-legal per B3), System.IO/System.IO.Compression, System.Text.Json, and HistoryInfo/ WordBorderInfo, both already in Core.Windows. Its only non-BCL call (AutomationProfile.GetTemporaryDirectory()) has been internal-and-Core since 6a, and Core.Windows already has InternalsVisibleTo from Core for exactly this kind of call. FileAssociationUtilities.cs needed one substitution, the same one 4e/6a established for this exact pattern: AppUtilities.IsPackaged() -> PackageIdentity.IsPackaged(). Everything else (Microsoft.Win32.Registry, FileUtilities.GetExePath(), GrabFrameFileUtilities.GrabFrameFileExtension) was already Core.Windows-legal. Text-Grab.csproj's InternalsVisibleTo grant to itself is irrelevant now that the class lives in Core.Windows, which already grants InternalsVisibleTo("Text-Grab"), so the app's one call site (App.xaml.cs) needed no change. That move also retired Text-Grab/Utilities/OpenDocumentFilterUtilities.cs, which existed for exactly one reason - GetOpenDocumentFilter() needed GrabFrameFileUtilities, which needed to stay app-side. With that gone, GetOpenDocumentFilter() folded back into FileUtilities.GetOpenDocumentFilter() in Core.Windows (its two now-unused-elsewhere helpers, GetExtensionsFilterPattern and GetVisualDocumentFilterPattern, went back to private), and the two call sites (App.xaml.cs, EditTextWindow.xaml.cs) call FileUtilities directly. Tests followed the production code: GrabFrameFileTests.cs (13 test cases, all already plain [Fact]/[Theory] with no WPF dependency - its `using System.Windows;` was unused even before this move) moved wholesale to Tests.Core.Windows. GetOpenDocumentFilter_IncludesVisualAndTextOptions moved from Tests/FilesIoTests.cs into Tests.Core.Windows/FileUtilitiesTests.cs alongside the other FileUtilities filter test. Net effect on the gate: Tests 475 (was 489), Tests.Core.Windows 158 (was 144), same 14 cases moved across, total still 1516. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Tests.Core.Windows/FileUtilitiesTests.cs | 16 +++++++ .../GrabFrameFileTests.cs | 7 ++- Tests/FilesIoTests.cs | 20 ++------- .../Utilities/FileAssociationUtilities.cs | 2 +- .../Utilities/FileUtilities.cs | 44 ++++++++++++++----- .../Utilities/GrabFrameFileUtilities.cs | 0 Text-Grab/App.xaml.cs | 2 +- .../Utilities/OpenDocumentFilterUtilities.cs | 42 ------------------ Text-Grab/Views/EditTextWindow.xaml.cs | 2 +- 9 files changed, 62 insertions(+), 73 deletions(-) rename {Tests => Tests.Core.Windows}/GrabFrameFileTests.cs (95%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/FileAssociationUtilities.cs (98%) rename {Text-Grab => Text-Grab.Core.Windows}/Utilities/GrabFrameFileUtilities.cs (100%) delete mode 100644 Text-Grab/Utilities/OpenDocumentFilterUtilities.cs diff --git a/Tests.Core.Windows/FileUtilitiesTests.cs b/Tests.Core.Windows/FileUtilitiesTests.cs index eed5806c..8dc1dbee 100644 --- a/Tests.Core.Windows/FileUtilitiesTests.cs +++ b/Tests.Core.Windows/FileUtilitiesTests.cs @@ -17,4 +17,20 @@ public void GetVisualDocumentFilter_IncludesPdfSupport() Assert.Contains("PDF files|*.pdf", filter); Assert.Contains("Image files|", filter); } + + // Joined FileUtilities in 7b once GrabFrameFileUtilities followed HistoryInfo to + // Core.Windows and GetOpenDocumentFilter() no longer needed the app-side + // OpenDocumentFilterUtilities split. + [Fact] + public void GetOpenDocumentFilter_IncludesVisualAndTextOptions() + { + string filter = FileUtilities.GetOpenDocumentFilter(); + + Assert.Contains("Supported documents|", filter); + Assert.Contains("Image and PDF files|", filter); + Assert.Contains("Spreadsheet documents|*.csv;*.tsv;*.tab", filter); + Assert.Contains("Markdown documents|*.md;*.markdown", filter); + Assert.Contains("Text documents (*.txt)|*.txt", filter); + Assert.Contains("All files (*.*)|*.*", filter); + } } diff --git a/Tests/GrabFrameFileTests.cs b/Tests.Core.Windows/GrabFrameFileTests.cs similarity index 95% rename from Tests/GrabFrameFileTests.cs rename to Tests.Core.Windows/GrabFrameFileTests.cs index 50577017..4b51cfc2 100644 --- a/Tests/GrabFrameFileTests.cs +++ b/Tests.Core.Windows/GrabFrameFileTests.cs @@ -2,13 +2,16 @@ using System.IO; using System.IO.Compression; using System.Text.Json; -using System.Windows; using Text_Grab; using Text_Grab.Models; using Text_Grab.Utilities; -namespace Tests; +namespace Text_Grab.Tests.Core.Windows; +// Moved wholesale in 7b: GrabFrameFileUtilities followed HistoryInfo to Core.Windows once the +// HistoryInfo blocker cleared (a8591aa), and every assertion here is against that headless pair +// (GrabFrameFileUtilities, HistoryInfo/WordBorderInfo) with no WPF type in sight - the file had +// an unused `using System.Windows;` from before that move, since dropped. public class GrabFrameFileTests { [Fact] diff --git a/Tests/FilesIoTests.cs b/Tests/FilesIoTests.cs index 248e4735..3442ac41 100644 --- a/Tests/FilesIoTests.cs +++ b/Tests/FilesIoTests.cs @@ -9,9 +9,10 @@ namespace Tests; // App-coupled remainder of the original file (batch 7a). Its IoUtilities-only tests moved to // Tests.Core/IoUtilitiesTests.cs and its FileUtilities.GetVisualDocumentFilter test moved to -// Tests.Core.Windows/FileUtilitiesTests.cs. What is left here is blocked on WPF ([WpfFact]/ -// [WpfTheory] needing Xunit.StaFact, which cannot be referenced outside Tests) or on the -// app-side OpenDocumentFilterUtilities/App classes, so it kept the original FilesIoTests name. +// Tests.Core.Windows/FileUtilitiesTests.cs. GetOpenDocumentFilter_IncludesVisualAndTextOptions +// followed it there in 7b, once GrabFrameFileUtilities stopped needing the app to build that +// filter. What is left here is blocked on WPF ([WpfFact]/[WpfTheory] needing Xunit.StaFact, +// which cannot be referenced outside Tests) or on the app-side members. public class FilesIoTests { private const string fontSamplePath = @"Images\font_sample.png"; @@ -102,19 +103,6 @@ public async Task ReadNotExistingImageFileEmpty(FileStorageKind storageKind) Assert.Null(emptyReturn); } - [Fact] - public void GetOpenDocumentFilter_IncludesVisualAndTextOptions() - { - string filter = OpenDocumentFilterUtilities.GetOpenDocumentFilter(); - - Assert.Contains("Supported documents|", filter); - Assert.Contains("Image and PDF files|", filter); - Assert.Contains("Spreadsheet documents|*.csv;*.tsv;*.tab", filter); - Assert.Contains("Markdown documents|*.md;*.markdown", filter); - Assert.Contains("Text documents (*.txt)|*.txt", filter); - Assert.Contains("All files (*.*)|*.*", filter); - } - [WpfFact] public void GetDroppedFilePaths_ReturnsExistingFilesOnly() { diff --git a/Text-Grab/Utilities/FileAssociationUtilities.cs b/Text-Grab.Core.Windows/Utilities/FileAssociationUtilities.cs similarity index 98% rename from Text-Grab/Utilities/FileAssociationUtilities.cs rename to Text-Grab.Core.Windows/Utilities/FileAssociationUtilities.cs index 9a501cd8..0e74e25e 100644 --- a/Text-Grab/Utilities/FileAssociationUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/FileAssociationUtilities.cs @@ -24,7 +24,7 @@ internal static class FileAssociationUtilities ///

internal static void EnsureGrabFrameFileAssociation() { - if (AppUtilities.IsPackaged()) + if (PackageIdentity.IsPackaged()) return; string executablePath = FileUtilities.GetExePath(); diff --git a/Text-Grab.Core.Windows/Utilities/FileUtilities.cs b/Text-Grab.Core.Windows/Utilities/FileUtilities.cs index e916a53d..28329f16 100644 --- a/Text-Grab.Core.Windows/Utilities/FileUtilities.cs +++ b/Text-Grab.Core.Windows/Utilities/FileUtilities.cs @@ -53,11 +53,38 @@ public static string GetVisualDocumentFilter() }); } - // GetOpenDocumentFilter() is NOT here: it also needs GrabFrameFileUtilities. - // GrabFrameFileUtilities stays app-side (blocked on HistoryInfo - see its section 7 row), so - // that filter builder lives in the app as OpenDocumentFilterUtilities.GetOpenDocumentFilter(), - // which calls back into GetVisualDocumentFilterPattern() and GetExtensionsFilterPattern() - // below (internal, for exactly that caller). + /// + /// The FileDialog filter string for the app's general "Open" dialog: images, PDFs, Grab + /// Frame files, spreadsheets, markdown and plain text. Folded back in here once + /// followed to + /// Core.Windows - before that, this lived app-side as + /// OpenDocumentFilterUtilities.GetOpenDocumentFilter() for exactly that reason. + /// + public static string GetOpenDocumentFilter() + { + string spreadsheetExtensions = GetExtensionsFilterPattern(IoUtilities.SpreadsheetExtensions); + string markdownExtensions = GetExtensionsFilterPattern(IoUtilities.MarkdownExtensions); + string grabFrameExtension = $"*{GrabFrameFileUtilities.GrabFrameFileExtension}"; + string supportedExtensions = string.Join(";", new[] + { + GetVisualDocumentFilterPattern(), + grabFrameExtension, + spreadsheetExtensions, + markdownExtensions, + "*.txt" + }.Where(pattern => !string.IsNullOrWhiteSpace(pattern))); + + return string.Join("|", new[] + { + $"Supported documents|{supportedExtensions}", + GetVisualDocumentFilter(), + GrabFrameFileUtilities.GetGrabFrameFileFilter(), + $"Spreadsheet documents|{spreadsheetExtensions}", + $"Markdown documents|{markdownExtensions}", + "Text documents (*.txt)|*.txt", + "All files (*.*)|*.*" + }); + } public static string GetPathToLocalFile(string imageRelativePath) { @@ -136,15 +163,12 @@ private static string GetImageExtensionsFilterPattern() return imageExtensions; } - // internal rather than private: OpenDocumentFilterUtilities (app-side, split out because it - // also needs GrabFrameFileUtilities) calls this to build its own filter pattern. - internal static string GetExtensionsFilterPattern(IEnumerable extensions) + private static string GetExtensionsFilterPattern(IEnumerable extensions) { return string.Join(";", extensions.Select(extension => $"*{extension}")); } - // internal for the same reason as GetExtensionsFilterPattern above. - internal static string GetVisualDocumentFilterPattern() + private static string GetVisualDocumentFilterPattern() { return string.Join(";", new[] { diff --git a/Text-Grab/Utilities/GrabFrameFileUtilities.cs b/Text-Grab.Core.Windows/Utilities/GrabFrameFileUtilities.cs similarity index 100% rename from Text-Grab/Utilities/GrabFrameFileUtilities.cs rename to Text-Grab.Core.Windows/Utilities/GrabFrameFileUtilities.cs diff --git a/Text-Grab/App.xaml.cs b/Text-Grab/App.xaml.cs index e877f490..9af38c47 100644 --- a/Text-Grab/App.xaml.cs +++ b/Text-Grab/App.xaml.cs @@ -84,7 +84,7 @@ public static async Task OpenFileWithPickerAsync(bool isQuiet = false) { OpenFileDialog openFileDialog = new() { - Filter = OpenDocumentFilterUtilities.GetOpenDocumentFilter(), + Filter = FileUtilities.GetOpenDocumentFilter(), Title = "Open File", CheckFileExists = true, InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) diff --git a/Text-Grab/Utilities/OpenDocumentFilterUtilities.cs b/Text-Grab/Utilities/OpenDocumentFilterUtilities.cs deleted file mode 100644 index 6b2ec2a6..00000000 --- a/Text-Grab/Utilities/OpenDocumentFilterUtilities.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Linq; - -namespace Text_Grab.Utilities; - -/// -/// Impure half of the "Open document" filter, split out of in the -/// deferred-ledger sweep because it also needs , which stays -/// app-side (blocked on HistoryInfo - see its section 7 row). Everything else that used to -/// live alongside it (, -/// ) moved to Text-Grab.Core.Windows under the original -/// FileUtilities name; this calls back into its two now-internal helpers, -/// and -/// , to build the same string it always did. -/// -public static class OpenDocumentFilterUtilities -{ - public static string GetOpenDocumentFilter() - { - string spreadsheetExtensions = FileUtilities.GetExtensionsFilterPattern(IoUtilities.SpreadsheetExtensions); - string markdownExtensions = FileUtilities.GetExtensionsFilterPattern(IoUtilities.MarkdownExtensions); - string grabFrameExtension = $"*{GrabFrameFileUtilities.GrabFrameFileExtension}"; - string supportedExtensions = string.Join(";", new[] - { - FileUtilities.GetVisualDocumentFilterPattern(), - grabFrameExtension, - spreadsheetExtensions, - markdownExtensions, - "*.txt" - }.Where(pattern => !string.IsNullOrWhiteSpace(pattern))); - - return string.Join("|", new[] - { - $"Supported documents|{supportedExtensions}", - FileUtilities.GetVisualDocumentFilter(), - GrabFrameFileUtilities.GetGrabFrameFileFilter(), - $"Spreadsheet documents|{spreadsheetExtensions}", - $"Markdown documents|{markdownExtensions}", - "Text documents (*.txt)|*.txt", - "All files (*.*)|*.*" - }); - } -} diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index 20beecba..d5e24812 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -4426,7 +4426,7 @@ private void OpenFileMenuItem_Click(object sender, RoutedEventArgs e) { // Set filter for file extension and default file extension DefaultExt = ".txt", - Filter = OpenDocumentFilterUtilities.GetOpenDocumentFilter(), + Filter = FileUtilities.GetOpenDocumentFilter(), DefaultDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) }; From 688f100d119c94ce6ab9c71d58cee73c931ab2e8 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 11:13:19 -0500 Subject: [PATCH 52/54] Plan: re-derive the deferred ledger and never-move list against B2 Two things had gone stale in docs/Core-Split-Plan.md since B2 (geometry currency) landed and since a8591aa moved HistoryInfo out of the app: the never-move list was written before either event, and section 7 carried rows whose blockers needed re-verifying against the current tree rather than whatever batch first wrote them down. Section 7: resolves the FileAssociationUtilities.cs and GrabFrameFileUtilities.cs rows (both moved in 3f4b222, recorded here with what the move actually needed). Re-verified the other five rows by reading each file again - all five keep the blocker they were already recorded with (OcrSourceUtilities's BitmapImage, SettingsService's ButtonInfo/ShortcutKeySet clone, GrabTemplateManager/Executor's split-plus-facade need, PdfDocumentRenderer's BitmapSource return). Adds two new rows this sweep found: WindowSelectionUtilities/WindowSelectionCandidate and GrabFrameViewScaleUtilities were sitting on the never-move list without ever being checked against B2 - both turn out to be pure Rect/Point/Size math with no other WPF coupling, the same shape B2 already solved for WordBorderInfo and TemplateRegion. Not moving them here (invariant 5 - the conversion surface at their call sites is real work, not a leaf), just recording that the blocker is gone and what is left to do. Never-move list: every remaining entry was re-read and annotated with the actual type it is blocked on, rather than left as a bare filename. The three UI-Automation overlay models (UiAutomationOptions/Item/Snapshot) turned out to be pure Rect/Point data too, same as the two files above, but they stay on the list anyway - their only consumers are UIAutomationUtilities (a real B4 blocker: System.Windows.Automation) and the views directly, so moving three data models would free nothing. WindowSelectionCandidate is different: its consumer, WindowSelectionUtilities, has no such second blocker, so it moved to section 7 instead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- docs/Core-Split-Plan.md | 66 ++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 4d21bb46..20f1ad76 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -644,22 +644,55 @@ against B2; update this document with the final layer map. ### Never moves — verified +Re-derived against B2 in 7b: every entry below was re-read against the current tree, not carried +forward from whenever it was first listed. `HistoryInfo` moving to Core.Windows in `a8591aa` is +exactly the kind of event that can quietly invalidate an old reason, so each file was checked for +that specifically as well as for the B2 (`Rect`/`Point`/`Size`) currency question. Two Utilities +entries and one Models entry did not survive the re-check and moved to §7 below; +everything else here still has the blocker it was originally listed for. + `Views/`, `Controls/`, `Pages/`, `Styles/`, `Themes/`, `App.xaml.cs`, `AssemblyInfo.cs`, `WPFExtensionMethods.cs`, `Properties/Settings.Designer.cs`, `TextGrabNotificationActivator.cs`. **Extensions:** `ControlExtensions`, `DapploExtensions`, `KeyboardExtensions`, `ShapeExtensions`. -**Utilities:** `ColorHelper`, `CursorClipper`, `WindowResizer`, `WindowUtilities`, -`GrabFrameViewScaleUtilities`, `NotificationUtilities`, `WindowSelectionUtilities`, `HotKeyManager`, -`AutomationDiagnostics`, `NotifyIconUtilities`, `OutputUtilities`, `ShareTargetUtilities`, -`ImplementAppOptions`, `MagickHelpers`, `CameraCaptureUtilities`, `SettingsImportExportUtilities`, -`DiagnosticsUtilities`, `PostGrabActionManager`, `CustomBottomBarUtilities`, `ShortcutKeysUtilities`, -`UIAutomationUtilities` (see B4). +**Utilities:** `ColorHelper` (`System.Windows.Media.Color`/`SolidColorBrush`, not a B2 type), +`CursorClipper` (`FrameworkElement`), `WindowResizer` (`Window`), `WindowUtilities` (`Window`, +`Application.Current.Windows`), `NotificationUtilities` (`Application.Current.Windows`, +`EditTextWindow`), `HotKeyManager` (`System.Windows.Forms.Keys` plus an `HwndSource`-style +message loop), `AutomationDiagnostics` (`Window`, `FrameworkElement`, `EventManager`), +`NotifyIconUtilities` (`Application`, `BitmapImage`, `Views`), `OutputUtilities` (`TextBox`, +`Clipboard`), `ShareTargetUtilities` (`Views`, WinRT share-target activation), +`ImplementAppOptions` (casts to the WPF `App`, calls `NotifyIconUtilities`), `MagickHelpers` +(`ImageSource`-typed signatures via `Magick.NET.SystemWindowsMedia`), `CameraCaptureUtilities` +(`Wpf.Ui.Controls.MessageBox`, needs a WPF `Window` for its `hwnd`), `SettingsImportExportUtilities` +(reflects over the entire settings surface by design), `DiagnosticsUtilities` (reads ~70 settings +properties, aggregates every deferred subsystem), `PostGrabActionManager` (`Wpf.Ui.Controls`, +`Wpf.Ui.Controls.MessageBox`), `CustomBottomBarUtilities` (`Text_Grab.Controls.CollapsibleButton`), +`ShortcutKeysUtilities` (`System.Windows.Input.Key`), `UIAutomationUtilities` +(`System.Windows.Automation` / `UIAutomationClient.dll`, see B4). + +`GrabFrameViewScaleUtilities` and `WindowSelectionUtilities` came off this list in 7b — see §7. +Both turned out to be pure `Rect`/`Point`/`Size` math with no other WPF coupling, which B2 already +has a conversion path for; they were never independently blocked, they were just never audited. **Models:** `ButtonInfo` (~90 static entries each assigning `Wpf.Ui.Controls.SymbolRegular` — whole-class, not splittable), `ShortcutKeySet` (`System.Windows.Input.Key`), `PostGrabContext`, -`FullscreenCaptureResult`, `UiAutomationOptions`, `UiAutomationOverlayItem`, -`UiAutomationOverlaySnapshot`, `WindowSelectionCandidate`, `LookupItem`. +`FullscreenCaptureResult` (both carry a `System.Windows.Media.Imaging.BitmapSource` — a B3 +blocker, independent of and unaffected by B2), `LookupItem` (`Wpf.Ui.Controls.SymbolRegular`, +fully qualified, plus a `HistoryInfo` constructor parameter that is incidental to its real +blocker). + +`UiAutomationOptions`, `UiAutomationOverlayItem`, `UiAutomationOverlaySnapshot` — re-checked +against B2 in 7b and found to be pure `Rect`/`Point` data records, the same shape B2 already +converted for `WordBorderInfo`/`TemplateRegion`. They stay here anyway: their only consumers are +`UIAutomationUtilities` (blocked on `System.Windows.Automation`, B4 already declined to chase +this) and the views (`FullscreenGrab.SelectionStyles.cs`, `GrabFrame.xaml.cs`) directly. Moving +three data models would not free anything real, so B4's "not worth weakening the tier boundary +to move one file" verdict extends to these models too - it was really always about them. +`WindowSelectionCandidate` used to sit in this same bucket by association +(`UiAutomationOverlaySnapshot.TargetWindow` is one), but 7b found it and its own consumer, +`WindowSelectionUtilities`, have no B4-style second blocker between them - see §7. **UndoRedoOperations:** all of them — `Operation`, `AddWordBorder`, `RemoveWordBorder`, `ChangedImage`, `UndoRedo`, `ChangeWord`, `ResizeWordBorder`. Every one is typed on `WordBorder`, @@ -812,17 +845,28 @@ constructor. ## 7. Deferred ledger Files with a real blocker, and the specific thing that clears it. **Movers append here.** -Every row below was verified by reading the file, not inferred. +Every row below was verified by reading the file, not inferred. 7b re-read every row against the +current tree (see 7b's own commits for what that turned up) rather than trusting the wording +inherited from whichever batch wrote it. + +**`FileAssociationUtilities.cs` and `GrabFrameFileUtilities.cs` resolved in `3f4b222`.** Both rows +said "unblocked by `a8591aa`, never audited past that" — this batch did that audit and found no +second blocker in either file. Both moved to Core.Windows unsplit; `FileAssociationUtilities.cs` +needed one substitution (`AppUtilities.IsPackaged()` → `PackageIdentity.IsPackaged()`, the +established 4e/6a pattern), `GrabFrameFileUtilities.cs` needed none. That move also retired +`OpenDocumentFilterUtilities.cs`, the app-side façade that existed only because +`GrabFrameFileUtilities` had to stay app-side — its one method folded back into +`FileUtilities.GetOpenDocumentFilter()` in Core.Windows in the same commit. | File | Blocker (specific) | Unblocked by | |---|---|---| | `Utilities/OcrSourceUtilities.cs` | Post-4c remainder. `LoadBitmapFromFile` builds a WPF `BitmapImage` to apply EXIF rotation; decoupling means a GDI+/WIC rewrite, and it takes `OcrAbsoluteFilePathAsync` and `OcrFile` with it. Engine dispatch additionally needs `WindowsAiUtilities` (5a) and `LanguageUtilities` (4e); the rest is `Window`/`BitmapSource` capture and stays | a GDI+/WIC rewrite, then 5a + 4e | -| `Utilities/FileAssociationUtilities.cs` | `GrabFrameExtensionKeyPath` is a `const` field initializer referencing `GrabFrameFileUtilities.GrabFrameFileExtension`, which requires the type at compile time. Still one level removed from the same root blocker, but that blocker is gone: it now follows `GrabFrameFileUtilities` whenever that moves | `GrabFrameFileUtilities` moving | -| `Utilities/GrabFrameFileUtilities.cs` | **Unblocked by `a8591aa`** — `HistoryInfo` is in Core.Windows now, so the public signature no longer pins this file to the app. Re-derive what is left of it before moving; it was never audited past the `HistoryInfo` blocker | nothing known — ready to attempt | | `Services/SettingsService.cs` | Clones `ButtonInfo` and `ShortcutKeySet` field-by-field (both never-move); `Windows.Storage.ApplicationDataContainer` caps it at Core.Windows regardless | needs a `ButtonInfo` redesign — likely never | | `Utilities/GrabTemplateManager.cs` | `SaveTemplateReferenceImage` (BitmapSource) and `CreateButtonInfoForTemplate` (Wpf.Ui) must stay; `IsFileBackedManagedSettingsEnabled` is a service property, not a scalar. `GrabTemplate`/`TemplateRegion` moved to Core in 2d, so the remaining blocker is a plain split | a split | | `Utilities/GrabTemplateExecutor.cs` | `LoadStoredRegexes()` needs a non-scalar seam. `GrabTemplate`/`TemplateRegion` moved to Core in 2d and 4c has landed, so the remaining blocker is the settings façade plus its calls into `OcrSourceUtilities` | a façade + `OcrSourceUtilities` | | `Utilities/PdfDocumentRenderer.cs` | `RenderPageAsync` returns `BitmapSource` — a public API shape change affecting several views | 5f | +| `Utilities/WindowSelectionUtilities.cs`, `Models/WindowSelectionCandidate.cs` | Found in 7b's never-move re-derivation, not by a mover: neither has any blocker beyond B2. `WindowSelectionUtilities` is `OSInterop` P/Invoke (Core.Windows-legal since 3a) plus `System.Windows.Rect`/`Point` math with no WPF UI type in sight; `WindowSelectionCandidate` is a `Rect`+`Point` data record, the same shape B2 already resolved for `WordBorderInfo`/`TemplateRegion`. Not attempted here — invariant 5, and the conversion surface (both fields on `FullscreenGrab.SelectionStyles.cs`, one field on `UiAutomationOverlaySnapshot`) is real work, not a leaf | `Rect`/`Point` → `RectangleF`/`PointF`, plus the view-side conversions at both call sites | +| `Utilities/GrabFrameViewScaleUtilities.cs` | Same 7b discovery as the row above: pure `Rect`/`Size` math (`GetMinimumWindowRect`, `StepScale`, `CoerceScale`), already calling the app's `ShapeExtensions.IsGood(this Rect)` — Core already has the `RectangleF` equivalent (`RectangleFExtensions.IsGood`) from B2. One call site (`GrabFrame.xaml.cs:1151`) converts at the edge | `Rect`/`Size` → `RectangleF`/`SizeF` | ## 8. Verified dead code — free, zero-risk prep From 0e43f838f0939766b7cd5962be6a852e4e02c10d Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 11:13:57 -0500 Subject: [PATCH 53/54] Plan: record 7a's test-placement decisions and add the final layer map Two closeout items from the 7b brief that were still missing from the plan itself, not just from commit messages. Section 4.7 gains a "7a as executed" table: which of the 12 suites 7a left in Tests, and the specific reason each one stayed - Settings.Default reads, HistoryService being app-side by design, a fake that would need two engines, a collection fixture, WPF types, or a production file that has not moved yet. Two of those reasons (GrabFrameViewScaleUtilitiesTests, WindowSelectionUtilitiesTests) already needed updating in the same edit, since the companion commit (688f100) just found their production types are not actually never-move - worth stating plainly rather than leaving a stale "never" in a document someone will read after this branch is old. Also documents Tests.Core.Windows/FakeTextGrabSettings.cs as the template for any future Core-tier test that needs settings with no app assembly to fall back on - it was a real design decision, not a one-off, and belongs in the plan. A "7b as executed" paragraph closes out the batch: what shim removal and the ledger re-derivation actually did, and where the final layer map lives. Section 9 (definition of done) gets an honest pass/fail against 7b, with an explicit callout that the MSIX wapproj build remains unverified - it needs a full Visual Studio install, which nothing in this chain of agents has had access to at any point. New section 10 (final layer map) is the piece written for someone with no memory of this conversion: what each of the three tiers holds and why, the four delegate-resolver seams (SettingsAccess, UiThreadAccess, InputLanguageAccess, TtsEngineAccess) and how each is registered via [ModuleInitializer], the ITextGrabSettings surface (20 members, the rule that kept it from sprawling), where TierBoundaryTests.cs enforces the tier boundary and what it actually checks, and a grouped, honest accounting of what stayed in the app and the real reason each group is there - not a single "not done yet." Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- docs/Core-Split-Plan.md | 223 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 218 insertions(+), 5 deletions(-) diff --git a/docs/Core-Split-Plan.md b/docs/Core-Split-Plan.md index 20f1ad76..6fe9a4de 100644 --- a/docs/Core-Split-Plan.md +++ b/docs/Core-Split-Plan.md @@ -638,10 +638,48 @@ plus the pure halves of `ProtocolUtilitiesTests` and `MarkdownDocumentUtilitiesT `Tests.Core.Windows`: `OcrTests` and the other headless-Windows suites. Delete `Tests.Core/ScaffoldingSmokeTests.cs`. +**7a as executed.** 12 suites moved unsplit and 6 more split across the boundary, landing in +`a3643e6`, `873ea58`, `5585199`, `fe4fd59`. What it deliberately left in `Tests`, and why, is the +standing record of test placement - re-verify against these reasons before moving any of them, +rather than assuming they are just unfinished: + +| Suite | Stays in `Tests` because | +|---|---| +| `LanguageServiceTests`, `CaptureLanguageUtilitiesTests` | Direct `Settings.Default` reads plus `[Collection("Settings isolation")]` | +| `HistoryServiceTests` | `HistoryService` is app-side by design after 6e | +| `TtsServiceTests` | Would need both a `TtsEngineAccess` fake and a `SettingsAccess` fake; judged non-trivial | +| `SettingsAccessTests` | `[Collection("Settings isolation")]` fixture | +| `WordBorderTests`, `ImageMethodsTests`, `FreeformCaptureUtilitiesTests` | WPF types, or entirely `[WpfFact]` | +| `PdfDocumentRendererTests` | The production file has not moved - blocked on 5f | +| `FullscreenCaptureResultTests` | Exercises `FullscreenCaptureResult`, a genuine never-move type (`BitmapSource`, B3) | +| `GrabFrameViewScaleUtilitiesTests`, `WindowSelectionUtilitiesTests` | Exercised types 7a judged never-move; 7b's re-derivation found neither production file has a real blocker beyond B2 currency (see §7) - moving the tests is still deferred pending that conversion, but the reason is no longer "never," it is "not yet" | + +7a also introduced `Tests.Core.Windows/FakeTextGrabSettings.cs`: a POCO `ITextGrabSettings` +(all 20 members, `Save()` a no-op) seeded from `Settings.settings`'s shipped defaults +(`RemoveFurigana = true` matters concretely - one OCR test's expected output depends on furigana +actually being filtered) and registered by a `[ModuleInitializer]`, the same mechanism the app +uses for `SettingsAccessInitializer`. It exists because `OcrUtilities.BuildTextFromOcrLines` reads +`SettingsAccess.Current` unconditionally, and `Tests.Core.Windows` has no app assembly to supply a +resolver the way `Tests` does. This is the template for any future Core-tier test suite that needs +settings and cannot reach the app: a local `ITextGrabSettings` double plus a `[ModuleInitializer]` +registration, not a dependency on the app assembly. + **7b — closeout.** Remove dead app-side shims; confirm the MSIX package still builds in Visual Studio (the one thing the CLI gate cannot check); re-derive the never-move list one final time against B2; update this document with the final layer map. +**7b as executed.** Shim removal: the dead `Table-Complex.png` content item and file (`6497dd3`, +confirmed dead by 7a's own commit message, not just by absence of new references), and +`GrabFrameFileUtilities.cs`/`FileAssociationUtilities.cs` moving to Core.Windows once audited and +found to have no second blocker, which in turn retired the app-side `OpenDocumentFilterUtilities.cs` +façade (`3f4b222`). The never-move re-derivation moved `WindowSelectionUtilities`, +`WindowSelectionCandidate` and `GrabFrameViewScaleUtilities` into §7 - all three were carried on the +list from an earlier wave without ever being checked against B2, and turned out to have no blocker +beyond it. Everything else on the list was re-verified and kept its existing reason (some reasons +were tightened with the actual type each file is blocked on, rather than left as a bare filename). +The MSIX package build could not be verified from this environment - see the note at the end of +§9. Final layer map: §10. + ### Never moves — verified Re-derived against B2 in 7b: every entry below was re-read against the current tree, not carried @@ -888,11 +926,186 @@ constant to WPF's `System.Windows.DataFormats.Bitmap`, and the file's only WinFo ## 9. Definition of done - `Text-Grab.Core` holds the text, pattern, table, calculation, and template logic, with no - `System.Windows`, no `Windows.*`, no P/Invoke. + `System.Windows`, no `Windows.*`, no P/Invoke. **Done.** - `Text-Grab.Core.Windows` holds OCR engines, capture, imaging, Windows AI, and Win32 interop, - with `UseWPF=false` still set. + with `UseWPF=false` still set. **Done** - `TierBoundaryTests` enforces the flag and the + reference direction on every test run. - `Text-Grab` holds Views, Controls, Pages, app wiring, and thin adapters — nothing else. + **Mostly true, honestly assessed in §10's residue list** - a handful of files stay app-side for + reasons stronger than "not yet gotten to it" (settings orchestration, `ButtonInfo`/`Wpf.Ui` + coupling, `System.Windows.Automation`), and §7 lists three more that are B2-only and simply + never scheduled. - `Tests.Core` runs in ~2s with no display and covers the pure tier; `Tests` keeps only the - WPF/STA tests. -- CI runs all three test projects; the MSIX package builds in Visual Studio. -- Section 7 is empty, or every remaining row has a written reason it stays. + WPF/STA tests (plus the handful of suites in §4.7's 7a table that could not split further). + **Done.** +- CI runs all three test projects. **Done for the three CLI-buildable projects.** The MSIX + package build is the one item this whole plan cannot verify from a CLI-only environment - see + the callout at the end of this section. +- Section 7 is empty, or every remaining row has a written reason it stays. **True as of 7b** - + seven rows remain, each re-verified against the current tree rather than carried forward from + whichever batch first wrote it (see §7's own note). + +**The MSIX wapproj build remains unverified.** Every gate command in §2 targets a project that +builds under the plain `dotnet` CLI; `Text-Grab-Package.wapproj` does not; and no agent that has +worked on this plan, across any wave, has had access to a full Visual Studio install to build it. +That means the packaging reference graph - three projects deep, each carrying its own +`RuntimeIdentifiers` and `PackageReference` list - has been exercised only by the risk-register +mitigation in §6 ("grep the csprojs at wave boundaries"), never by an actual wapproj build. Open +the solution in Visual Studio and build the `Text-Grab-Package` project before shipping a release +off this branch; that is the one remaining step this document cannot close out for you. + +## 10. Final layer map + +Written for whoever opens this repo next with no memory of any of the above. The short version: +three library tiers, dependencies point one way, four small seams carry the app-only behavior +that pure/headless code still needs to call, and a reflection-based test enforces the boundary so +a future PR cannot quietly undo it. + +### The tiers + +``` +Text-Grab.Core net10.0 pure logic, no UI, no Windows + ^ +Text-Grab.Core.Windows net10.0-windows10.0.22621.0 WinRT / GDI+ / P-Invoke, UseWPF=false + ^ +Text-Grab net10.0-windows... WPF app Views, Controls, Pages, app wiring +``` + +Dependencies point one way only: app -> Core.Windows -> Core. Core never references Core.Windows +and neither library references the app - this is invariant 3, and it is the one a move is most +likely to violate by accident (a call site left behind, a `using` that should not resolve). It is +enforced mechanically, not just by convention: see "The tier guard" below. + +**`Text-Grab.Core`** (57 files) holds everything that needs nothing but the BCL: text and pattern +matching (`PatternExecutor`, `ColumnSplitUtilities`, `RecognizerExecutor`, regex/number/markdown +utilities), the table and calculation engines (`ResultTable`'s clustering algorithm, +`CalculationService` and its ~2000 lines built on NCalcAsync/UnitsNet), the geometry currency +(`RectangleFExtensions`), the settings-provider chain (`AutomationProfile`, +`AutomationSettingsProvider`, riding on `System.Configuration.ConfigurationManager` - verified to +resolve and run on plain net10.0 in Wave 0), the portable models (`WordBorderInfo`, +`TemplateRegion`, `GrabTemplate`, the CF_HTML table parser), `Enums.cs` (all 17, merged in 2a), +and the four delegate-resolver seams described below. Package references: `Markdig`, the +`Microsoft.Recognizers.Text.*` family, `NCalcAsync`, `UnitsNet`, +`System.Configuration.ConfigurationManager`. + +**`Text-Grab.Core.Windows`** (47 files) holds everything that needs Windows but not WPF: OCR +(`OcrUtilities`, the Windows AI / language chain, `TesseractHelper`), capture and imaging +(`HdrScreenCapture`, the headless half of `ImageMethods`, `SoftwareBitmapExtensions`, +`ImageChangeDetector`), the file and history pipelines (`FileUtilities`, +`GrabFrameFileUtilities`, `FileAssociationUtilities`, `HistoryFileUtilities`, +`HistoryInfo`), the audio transcription chain (`AudioTranscriptionUtilities`, NAudio + Whisper.net, +moved wholesale in 6c), text-to-speech (`WindowsSpeechEngine`), Win32/WinRT interop +(`OSInterop`, `RegistryMonitor`, `NativeMethods`, `DesktopNotificationManagerCompat`), and the +HDR/D3D11 pipeline. `UseWPF=false` is load-bearing here, not decorative - it is what keeps this +tier usable from a headless host, and B4 declined a real `FrameworkReference` loophole +specifically to protect it. Package references: `Microsoft.WindowsAppSDK.AI`, +`ZXing.Net.Bindings.Windows.Compatibility`, `CliWrap`, `Vortice.Direct3D11`/`Vortice.DXGI`, +`Magick.NET-Q16-AnyCPU`/`Magick.NET.SystemDrawing`, `NAudio`, `Whisper.net`/`Whisper.net.Runtime`. + +**`Text-Grab`** (115 files) holds Views, Controls, Pages, app wiring, and the residue described +below - WPF-bound code, settings orchestration, and the handful of files that were never worth +splitting for what they would free. See the residue list at the end of this section for the +honest accounting of what is here and why, rather than a claim that it is all just "not done yet." + +### The four delegate-resolver seams + +Four places in `Text-Grab.Core/Services/` let portable code call something only the app can +provide, without Core referencing the app. Same shape every time: a static class holding a +delegate field, a `SetResolver`/`SetPoster` setter, and a getter that throws +`InvalidOperationException` if nothing has registered yet. Each is registered from a +`[ModuleInitializer]` in `Text-Grab/Utilities/`, not from `App.appStartup` - the `Tests` project +loads the `Text-Grab` assembly and exercises its code without ever raising the WPF Startup event, +so a module initializer is the only registration point that covers both the running app and the +test host. `Tests.Core.Windows` has no app assembly at all, so tests that exercise a seam from +there register their own fake (see `FakeTextGrabSettings` below). + +| Seam | Registered by | Points at | +|---|---|---| +| `SettingsAccess` (`Func`) | `SettingsAccessInitializer` | `AppUtilities.TextGrabSettings` (which resolves `Singleton.Instance.ClassicSettings` lazily) | +| `UiThreadAccess` (a poster, not a resolver) | `UiThreadAccessInitializer` | `Application.Current?.Dispatcher.InvokeAsync(...)`, silently dropping the post if there is no dispatcher yet | +| `InputLanguageAccess` (`Func`) | `InputLanguageAccessInitializer` | `InputLanguageManager.Current?.CurrentInputLanguage?.Name`, catching the `NullReferenceException` the manager can throw internally | +| `TtsEngineAccess` (`Func`, a factory not a stored instance) | `TtsEngineAccessInitializer` | `new WindowsSpeechEngine()` | + +They arrived in this order as each wave hit the blocker they solve: `SettingsAccess` in B1 (Wave +0), `UiThreadAccess` in 5b (`HdrScreenCapture`'s one-time consent-dialog pump), +`InputLanguageAccess` in 4e (`LanguageService`'s only non-WinRT read), `TtsEngineAccess` in 6b +(`TtsService`'s engine field initializer). If a future move hits the same shape - portable code +needs one small thing only the app can answer - this is the pattern to reach for before reaching +for a bigger redesign. + +### `ITextGrabSettings` + +`Text-Grab.Core/Interfaces/ITextGrabSettings.cs` is the slice of the app's 104-property +`Settings` class that portable/headless code is allowed to read - 19 properties plus `Save()`, +20 members, up from the 10 (`CorrectErrors`/`CorrectToLatin`/... plus `Save()`) it started with in +B1. The app implements it by declaring it on the existing hand-written `Settings` partial +(`internal sealed partial class Settings : ITextGrabSettings`) - the generated properties already +match on name and type, so there is no forwarding code on that side. Every addition after B1 came +from a real move that needed it (tracked in the consolidated-additions table in §4.6) and every +one already existed in `Settings.settings`, so none needed a `.settings` edit. The rule that kept +it from sprawling: add a property only when a move demands one; a file needing more than a +handful of new members wants a façade split instead (`PatternItemCatalog`, +`WebSearchUrlCatalog`), not an interface that tries to cover it. + +A Core-tier or Core.Windows-tier test that needs settings and has no app assembly to fall back on +cannot use `SettingsAccessInitializer` - `Tests.Core.Windows/FakeTextGrabSettings.cs` is the +template for that case: a minimal `internal sealed class` implementing `ITextGrabSettings` with +every default copied from `Settings.settings`'s shipped profile, registered by its own +`[ModuleInitializer]`. It exists because `OcrUtilities.BuildTextFromOcrLines` reads +`SettingsAccess.Current` unconditionally and a handful of moved `OcrTests` methods exercise that +path headlessly. + +### The tier guard + +`Tests.Core.Windows/TierBoundaryTests.cs` enforces invariants 2 and 3 by reflection, not by +convention: it loads the `Text-Grab.Core` and `Text-Grab.Core.Windows` assemblies and asserts +`GetReferencedAssemblies()` contains none of `PresentationCore`, `PresentationFramework`, +`WindowsBase`, `System.Xaml` (checked on both), plus `System.Windows.Forms`, +`System.Drawing.Common`, and anything prefixed `Microsoft.Windows.`/`Microsoft.WindowsAppSDK` +(checked on Core only, since Core.Windows legitimately needs Windows APIs - just not WPF ones); +and that Core does not reference Core.Windows at all. This is what makes a `UseWPF` flip or a +stray `WindowsBase` pull-in (the B4 `FrameworkReference` loophole) a test failure instead of +something that only surfaces later as an unexplained csproj diff in review. + +### Test tiers + +`Tests.Core` (17 files, net10.0, no display, ~2s) mirrors `Text-Grab.Core`. `Tests.Core.Windows` +(12 files, net10.0-windows, headless - no `Xunit.StaFact`, since that package pulls in +`WindowsBase`, which the tier guard bans) mirrors `Text-Grab.Core.Windows`. `Tests` (54 files, +WPF/STA, references the app) keeps what genuinely needs a WPF host, plus the suites listed in +§4.7's 7a table that could not follow their production code for a specific, still-true reason. + +### The honest residue - what stayed in the app, and why + +Not everything left in `Text-Grab` is a WPF view. Grouping the real reasons, so the answer to +"why is this still here" is always one of these, not a shrug: + +- **Actual WPF/WinForms UI types.** `Window`, `FrameworkElement`, `TextBox`, `Wpf.Ui.Controls.*`, + `System.Windows.Automation` (`UIAutomationClient.dll`, B4), `System.Windows.Forms.Keys`. This is + most of the never-move list in §4.0 and holds no surprises. +- **Settings orchestration that is deliberately whole-surface.** `SettingsImportExportUtilities` + and `DiagnosticsUtilities` each touch dozens of settings properties by design (reflection over + the entire surface, or a diagnostics dump of most of it) - the kind of file + `ITextGrabSettings`'s "add one property per real need" rule is specifically meant to keep out of + Core. +- **`BitmapSource` currency (B3).** `PostGrabContext`, `FullscreenCaptureResult`, + `PdfDocumentRenderer.RenderPageAsync`, and the WPF half of `ImageMethods`/`OcrSourceUtilities` + all carry `System.Windows.Media.Imaging.BitmapSource`, which B3 fixed as app-only currency; + `OcrSourceUtilities.LoadBitmapFromFile` specifically needs a GDI+/WIC rewrite to lose it, not + just a move. +- **`ButtonInfo`/`ShortcutKeySet` coupling.** `SettingsService` clones both field-by-field; + `ButtonInfo` itself is ~90 static entries each assigning a `Wpf.Ui.Controls.SymbolRegular`, not + a splittable class. `GrabTemplateManager`'s `CreateButtonInfoForTemplate` inherits the same + blocker. +- **B2-only, simply never scheduled.** Three files (`WindowSelectionUtilities`, + `WindowSelectionCandidate`, `GrabFrameViewScaleUtilities`) turned out in 7b to have no blocker + beyond the `Rect`/`Point`/`Size` currency B2 already solved for `WordBorderInfo`/`TemplateRegion` + - they were carried on the never-move list without ever being checked against it. They are the + most likely next wave if this plan is picked back up; see their §7 rows for the exact + conversion surface. +- **A façade with no interface change.** `PatternItemCatalog`, `WebSearchUrlCatalog` - the + settings-touching half of a file whose pure half already moved, kept deliberately thin rather + than folded into `ITextGrabSettings`. +- **Genuinely unresolved, per §7.** `GrabTemplateManager`/`GrabTemplateExecutor` (need a split + plus a non-scalar settings seam for `Load*`/`Save*`-shaped methods, which `ITextGrabSettings` + deliberately does not cover) and `SettingsService` itself (judged likely-never, see above). \ No newline at end of file From 1f0d9216fa13547ac7f506dce98232104440997a Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 30 Aug 2026 11:32:13 -0500 Subject: [PATCH 54/54] 7a fix: map Tests.Core.Windows to its real solution platforms Tests.Core.Windows was added with every platform-specific configuration pointing at Debug|Any CPU / Release|Any CPU, but the project declares Platforms x64;x86;ARM64 and has no Any CPU configuration, so Visual Studio flagged the mappings on every solution load. Map them platform-for-platform, matching how Tests (same Platforms set) is already wired. The four Any CPU rows stay as-is, also matching Tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv --- Text-Grab.sln | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Text-Grab.sln b/Text-Grab.sln index 71716c68..79297ea4 100644 --- a/Text-Grab.sln +++ b/Text-Grab.sln @@ -154,20 +154,20 @@ Global {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|x86.Build.0 = Release|Any CPU {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F96646AB-DAD1-4A0E-AE95-3AA24A8754D5}.Release|Any CPU.Build.0 = Release|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|ARM64.Build.0 = Debug|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x64.ActiveCfg = Debug|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x64.Build.0 = Debug|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x86.ActiveCfg = Debug|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x86.Build.0 = Debug|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|ARM64.Build.0 = Debug|ARM64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x64.ActiveCfg = Debug|x64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x64.Build.0 = Debug|x64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x86.ActiveCfg = Debug|x86 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|x86.Build.0 = Debug|x86 {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5F8E212A-3C21-46EF-97EF-7B890C112873}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|ARM64.ActiveCfg = Release|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|ARM64.Build.0 = Release|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x64.ActiveCfg = Release|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x64.Build.0 = Release|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x86.ActiveCfg = Release|Any CPU - {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x86.Build.0 = Release|Any CPU + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|ARM64.ActiveCfg = Release|ARM64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|ARM64.Build.0 = Release|ARM64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x64.ActiveCfg = Release|x64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x64.Build.0 = Release|x64 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x86.ActiveCfg = Release|x86 + {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|x86.Build.0 = Release|x86 {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|Any CPU.ActiveCfg = Release|Any CPU {5F8E212A-3C21-46EF-97EF-7B890C112873}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection