From 561ea57d3e1906f9300a8528bbb354cb419ffd86 Mon Sep 17 00:00:00 2001 From: R0GGER Date: Tue, 10 Nov 2026 23:54:21 +0100 Subject: [PATCH] 2.0.0 --- CHANGELOG.md | 19 ++ README.md | 4 +- build.ps1 | 2 +- installer/Kniip.iss | 2 +- src/Kniip/App.xaml.cs | 58 ++++- src/Kniip/Editor/AnnotationSurface.cs | 2 +- src/Kniip/Editor/CanvasBackground.cs | 25 ++- src/Kniip/Editor/ColorPickerButton.xaml | 2 +- src/Kniip/Editor/EditorWindow.xaml | 40 +--- src/Kniip/Editor/EditorWindow.xaml.cs | 84 +++++-- src/Kniip/Editor/GradientAngleDial.xaml | 64 ++++++ src/Kniip/Editor/GradientAngleDial.xaml.cs | 250 +++++++++++++++++++++ src/Kniip/Editor/OcrResultWindow.xaml | 31 +++ src/Kniip/Editor/OcrResultWindow.xaml.cs | 110 +++++++++ src/Kniip/Editor/OcrService.cs | 162 +++++++++++++ src/Kniip/Infrastructure/TrayIcon.cs | 10 +- src/Kniip/Kniip.csproj | 2 +- src/Kniip/Settings/AppSettings.cs | 3 + src/Kniip/Settings/SettingsWindow.xaml | 9 +- src/Kniip/Settings/SettingsWindow.xaml.cs | 4 + 20 files changed, 820 insertions(+), 63 deletions(-) create mode 100644 src/Kniip/Editor/GradientAngleDial.xaml create mode 100644 src/Kniip/Editor/GradientAngleDial.xaml.cs create mode 100644 src/Kniip/Editor/OcrResultWindow.xaml create mode 100644 src/Kniip/Editor/OcrResultWindow.xaml.cs create mode 100644 src/Kniip/Editor/OcrService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 838069f..fe725a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan. +## 2.0.0 - 2026-11-10 + +### Nieuw + +- **OCR (tekst herkennen).**\ + Lees tekst van het scherm en kopieer hem. Naast de knoppen voor een schermafdruk en een screencast staat nu de knop **OCR**, met hetzelfde icoon als in het Knipprogramma. Klik erop, sleep een rechthoek, en de tekst daarin opent in een apart venster. Daar pas je hem aan en kopieer je alles, of alleen wat je selecteert. De editor gaat tijdens het kiezen even opzij en komt daarna terug. + - Druk in de editor op Ctrl+Shift+T om de tekst van het geopende knipsel te lezen, met je aantekeningen erbij. + - Je vindt OCR ook in het menu van het icoon in het systeemvak, onder **OCR (tekst herkennen)...**. Geef het in de instellingen onder **Sneltoetsen** een eigen sneltoets. + - kniip gebruikt de tekstherkenning van Windows, in de talen van je Windows-account. Vindt Windows geen taal voor tekstherkenning, dan staat in het venster hoe je er een toevoegt. +- **Verloop in elke richting.**\ + De richting van een verloop als achtergrond kies je nu met een draaiknop in plaats van uit vier vaste richtingen. Klik op de knop naast de kleuren en sleep de knop rond de cirkel. Je ziet het verloop meteen meedraaien. + - Hij klikt vast op de acht hoofdrichtingen. Houd Shift ingedrukt voor stappen van 15°, of Alt om niet vast te klikken. Je kunt ook de hoek in graden typen, of scrollen boven de knop. + - **Vanuit het midden** staat nu als knop onder de cirkel. + - Elke nieuwe richting is één stap in **Ongedaan maken**, ook als je lang sleept. + +### Gewijzigd + +- Het blokjespatroon achter transparante delen is lichter en daardoor rustiger om naar te kijken. + ## 1.1.9 - 2026-11-10 ### Gewijzigd diff --git a/README.md b/README.md index ad9bc14..e8cf0d6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee | Versie | Installer | | ------ | --------- | -| 1.1.9 | [Kniip-Setup-1.1.9.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.9/Kniip-Setup-1.1.9.exe) | +| 2.0.0 | [Kniip-Setup-2.0.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/2.0.0/Kniip-Setup-2.0.0.exe) | Oudere versies vind je bij [alle releases](https://git.hibbit.net/rogier/kniip/releases). @@ -80,7 +80,7 @@ Vereisten: Bouwen: ```powershell -.\build.ps1 # versie 1.1.9 +.\build.ps1 # versie 2.0.0 .\build.ps1 -Version 1.2.0 # andere versie ``` diff --git a/build.ps1 b/build.ps1 index 7bac010..1d14df6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -12,7 +12,7 @@ # .\build.ps1 -SkipTest -> zonder testexe param( - [string]$Version = "1.1.9", + [string]$Version = "2.0.0", [string]$Runtime = "win-x64", [switch]$SkipTest ) diff --git a/installer/Kniip.iss b/installer/Kniip.iss index b392a4d..d598a69 100644 --- a/installer/Kniip.iss +++ b/installer/Kniip.iss @@ -5,7 +5,7 @@ #define AppName "kniip" #define AppExe "Kniip.exe" #ifndef AppVersion - #define AppVersion "1.1.9" + #define AppVersion "2.0.0" #endif #ifndef PublishDir #define PublishDir "..\artifacts\publish" diff --git a/src/Kniip/App.xaml.cs b/src/Kniip/App.xaml.cs index e294fe2..0cbef51 100644 --- a/src/Kniip/App.xaml.cs +++ b/src/Kniip/App.xaml.cs @@ -21,6 +21,9 @@ namespace Kniip; public partial class App : Application { + /// Waar een rechthoekknipsel heen gaat: naar de editor of naar tekstherkenning. + private enum SnipTarget { Editor, Ocr } + private SingleInstance? _singleInstance; private TrayIcon? _tray; private SettingsWindow? _settingsWindow; @@ -28,6 +31,8 @@ public partial class App : Application private readonly List _snipWindows = new(); private List _snipSnapshot = new(); private CountdownWindow? _snipCountdown; + private SnipTarget _snipTarget; + private Action? _snipFinished; private uint? _ignoredClipboardSequence; public static App Instance => (App)Current; @@ -94,6 +99,7 @@ public partial class App : Application _tray.OpenFileRequested += OpenFileWithDialog; _tray.SnipRequested += BeginRegionSnip; _tray.WindowSnipRequested += BeginWindowSnip; + _tray.TextRecognitionRequested += () => BeginTextSnip(); _tray.SetCaptureDelay(Settings.CaptureDelaySeconds); _tray.CaptureDelayChanged += seconds => { @@ -205,6 +211,11 @@ public partial class App : Application timer.Tick += (_, _) => { timer.Stop(); + if (_snipCountdown == null && _snipWindows.Count == 0) + { + _snipTarget = SnipTarget.Editor; + } + BeginSnip(pickWindow: false); }; timer.Start(); @@ -216,8 +227,34 @@ public partial class App : Application /// Bevriest alle schermen (na de ingestelde vertraging) en laat een venster aanklikken; het resultaat opent in de editor. public void BeginWindowSnip() => BeginDelayedSnip(pickWindow: true); - private void BeginDelayedSnip(bool pickWindow) + /// Als een rechthoekknipsel, maar de herkende tekst opent in een apart venster in plaats van de editor. + /// Na het kiezen of annuleren, vóórdat het tekstvenster opent; bijvoorbeeld om de editor terug te zetten. + public void BeginTextSnip(Action? finished = null) { + if (_snipCountdown != null || _snipWindows.Count > 0 || Recorder.Stage == RecordingStage.Selecting) + { + finished?.Invoke(); + return; + } + + _snipFinished = finished; + BeginDelayedSnip(pickWindow: false, SnipTarget.Ocr); + } + + private void RunSnipFinished() + { + Action? finished = _snipFinished; + _snipFinished = null; + finished?.Invoke(); + } + + private void BeginDelayedSnip(bool pickWindow, SnipTarget target = SnipTarget.Editor) + { + if (_snipCountdown == null && _snipWindows.Count == 0) + { + _snipTarget = target; + } + if (_snipCountdown != null) { return; @@ -243,7 +280,11 @@ public partial class App : Application }; timer.Start(); }; - countdown.Cancelled += EndCountdown; + countdown.Cancelled += () => + { + EndCountdown(); + RunSnipFinished(); + }; _snipCountdown = countdown; ApplyHotkeys(); countdown.Start(); @@ -326,7 +367,14 @@ public partial class App : Application _snipWindows.Clear(); List snapshot = _snipSnapshot; _snipSnapshot = new(); - if (image != null) + SnipTarget target = _snipTarget; + _snipTarget = SnipTarget.Editor; + RunSnipFinished(); + if (image != null && target == SnipTarget.Ocr) + { + BringToFront(OcrResultWindow.Open(image)); + } + else if (image != null) { string? windowName = area is { } a ? PhysicalWindow.ProgramNameAt(snapshot, a) : null; OpenEditor(image, windowName); @@ -356,6 +404,7 @@ public partial class App : Application Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4)); Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif)); Bind(Settings.ClipboardHotkey, OpenFromClipboard); + Bind(Settings.OcrHotkey, () => BeginTextSnip()); if (_snipCountdown is { } countdown) { // Alleen tijdens het aftellen; daarna moet Esc gewoon naar het actieve programma. @@ -366,7 +415,8 @@ public partial class App : Application } _tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey), - HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey)); + HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey), + HotkeyDisplay(Settings.OcrHotkey)); } /// Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk. diff --git a/src/Kniip/Editor/AnnotationSurface.cs b/src/Kniip/Editor/AnnotationSurface.cs index 62c0560..175d5ce 100644 --- a/src/Kniip/Editor/AnnotationSurface.cs +++ b/src/Kniip/Editor/AnnotationSurface.cs @@ -15,7 +15,7 @@ public sealed class AnnotationSurface : FrameworkElement private static readonly Brush GhostHandleBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(170, 255, 255, 255))); private static readonly Brush CropShadeBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(120, 0, 0, 0))); private static readonly Brush CheckerLight = RenderHelpers.Freeze(new SolidColorBrush(Colors.White)); - private static readonly Brush CheckerDark = RenderHelpers.Freeze(new SolidColorBrush(Color.FromRgb(0xD9, 0xD9, 0xD9))); + private static readonly Brush CheckerDark = RenderHelpers.Freeze(new SolidColorBrush(Color.FromRgb(0xEF, 0xEF, 0xEF))); public static readonly Color GuideColor = Color.FromRgb(0, 180, 255); private static readonly Brush GuideBrush = RenderHelpers.Freeze(new SolidColorBrush(GuideColor)); diff --git a/src/Kniip/Editor/CanvasBackground.cs b/src/Kniip/Editor/CanvasBackground.cs index a33e2cb..c8dc5c8 100644 --- a/src/Kniip/Editor/CanvasBackground.cs +++ b/src/Kniip/Editor/CanvasBackground.cs @@ -7,21 +7,23 @@ namespace Kniip.Editor; public enum BackgroundKind { None, Color, Gradient, Image } -public enum GradientDirection { Vertical, Horizontal, Diagonal, Radial } +public enum GradientDirection { Linear, Radial } /// /// Wat er onder het knipsel ligt; alleen zichtbaar waar het knipsel transparant is. /// De kleuren en afbeelding blijven bewaard bij het wisselen van soort. /// +/// Richting van een lineair verloop in graden, met de klok mee vanaf boven: 90 = naar rechts, 180 = naar onder. public sealed record CanvasBackground( BackgroundKind Kind, Color Color, Color Color2, GradientDirection Direction, + double Angle, BitmapSource? Image) { public static readonly CanvasBackground Default = new( - BackgroundKind.None, Colors.White, Color.FromRgb(0x00, 0x78, 0xD4), GradientDirection.Vertical, null); + BackgroundKind.None, Colors.White, Color.FromRgb(0x00, 0x78, 0xD4), GradientDirection.Linear, 180, null); public bool IsVisible => Kind switch { @@ -51,13 +53,20 @@ public sealed record CanvasBackground( } } - private Brush CreateGradient() => Direction switch + private Brush CreateGradient() { - GradientDirection.Horizontal => new LinearGradientBrush(Color, Color2, new Point(0, 0.5), new Point(1, 0.5)), - GradientDirection.Diagonal => new LinearGradientBrush(Color, Color2, new Point(0, 0), new Point(1, 1)), - GradientDirection.Radial => new RadialGradientBrush(Color, Color2), - _ => new LinearGradientBrush(Color, Color2, new Point(0.5, 0), new Point(0.5, 1)) - }; + if (Direction == GradientDirection.Radial) + { + return new RadialGradientBrush(Color, Color2); + } + + // In de eenheidsruimte van het vlak, zodat 45° van hoek naar hoek loopt; de halve lengte reikt tot in de hoeken. + double radians = Angle * Math.PI / 180; + var direction = new Vector(Math.Sin(radians), -Math.Cos(radians)); + double half = (Math.Abs(direction.X) + Math.Abs(direction.Y)) / 2; + var center = new Point(0.5, 0.5); + return new LinearGradientBrush(Color, Color2, center - direction * half, center + direction * half); + } /// Afbeelding zo schalen dat ze het hele vlak vult; wat uitsteekt valt weg. private static Rect Cover(double imageWidth, double imageHeight, Rect area) diff --git a/src/Kniip/Editor/ColorPickerButton.xaml b/src/Kniip/Editor/ColorPickerButton.xaml index d8c5490..9cbce52 100644 --- a/src/Kniip/Editor/ColorPickerButton.xaml +++ b/src/Kniip/Editor/ColorPickerButton.xaml @@ -7,7 +7,7 @@ - + diff --git a/src/Kniip/Editor/EditorWindow.xaml b/src/Kniip/Editor/EditorWindow.xaml index 2c2b0cc..73f517f 100644 --- a/src/Kniip/Editor/EditorWindow.xaml +++ b/src/Kniip/Editor/EditorWindow.xaml @@ -288,7 +288,7 @@ - + @@ -326,32 +326,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -466,6 +440,15 @@ + @@ -724,8 +707,7 @@ ToolTip="Achtergrond onder de transparante delen" SelectionChanged="BackgroundKind_Changed" /> - + + + + + + + + + + + diff --git a/src/Kniip/Editor/OcrResultWindow.xaml.cs b/src/Kniip/Editor/OcrResultWindow.xaml.cs new file mode 100644 index 0000000..62c6874 --- /dev/null +++ b/src/Kniip/Editor/OcrResultWindow.xaml.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; + +namespace Kniip.Editor; + +/// Toont de herkende tekst van een afbeelding, om aan te passen en te kopiëren. +public partial class OcrResultWindow : Window +{ + private OcrResultWindow() + { + InitializeComponent(); + Infrastructure.ThemeManager.Attach(this); + } + + /// Klaar zodra de tekst herkend is of het herkennen is mislukt. + public Task Completion { get; private set; } = Task.CompletedTask; + + /// Opent het venster meteen en vult het zodra de tekst herkend is. + public static OcrResultWindow Open(BitmapSource image, Window? owner = null) + { + var window = new OcrResultWindow(); + if (owner is { IsVisible: true }) + { + window.Owner = owner; + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + } + + window.SetBusy(true); + window.Show(); + window.Activate(); + window.Completion = window.RecognizeAsync(image); + return window; + } + + private async Task RecognizeAsync(BitmapSource image) + { + try + { + ShowText(await OcrService.RecognizeAsync(image)); + } + catch (Exception ex) + { + ShowError(ex is InvalidOperationException ? ex.Message : $"Tekst herkennen is mislukt.\n\n{ex.Message}"); + } + finally + { + SetBusy(false); + } + } + + private void SetBusy(bool busy) + { + ResultBox.IsReadOnly = busy; + CopyButton.IsEnabled = !busy && ResultBox.Text.Length > 0; + if (busy) + { + StatusText.Text = "Tekst herkennen..."; + } + } + + private void ShowText(string text) + { + ResultBox.Text = text; + bool empty = text.Trim().Length == 0; + EmptyPanel.Visibility = empty ? Visibility.Visible : Visibility.Collapsed; + int lines = text.Split('\n').Count(line => line.Trim().Length > 0); + StatusText.Text = empty ? string.Empty + : $"{lines} {(lines == 1 ? "regel" : "regels")} herkend. Je kunt de tekst hier aanpassen."; + if (!empty) + { + ResultBox.Focus(); + ResultBox.SelectAll(); + } + } + + private void ShowError(string message) + { + ResultBox.Text = string.Empty; + ResultBox.IsEnabled = false; + EmptyText.Text = message; + EmptyPanel.Visibility = Visibility.Visible; + StatusText.Text = string.Empty; + } + + private void ResultBox_SelectionChanged(object sender, RoutedEventArgs e) + { + if (!IsLoaded) + { + return; + } + + CopyLabel.Text = ResultBox.SelectionLength > 0 && ResultBox.SelectionLength < ResultBox.Text.Length + ? "Selectie kopiëren" + : "Kopiëren"; + CopyButton.IsEnabled = !ResultBox.IsReadOnly && ResultBox.Text.Length > 0; + } + + private void Copy_Click(object sender, RoutedEventArgs e) + { + string text = ResultBox.SelectionLength > 0 ? ResultBox.SelectedText : ResultBox.Text; + StatusText.Text = ClipboardWriter.SetText(text) + ? "Gekopieerd naar het klembord." + : "Kopiëren mislukt: het klembord is bezet. Probeer het opnieuw."; + } + + private void Close_Click(object sender, RoutedEventArgs e) => Close(); +} diff --git a/src/Kniip/Editor/OcrService.cs b/src/Kniip/Editor/OcrService.cs new file mode 100644 index 0000000..47df9a6 --- /dev/null +++ b/src/Kniip/Editor/OcrService.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Windows.Globalization; +using Windows.Media.Ocr; +using BitmapAlphaMode = Windows.Graphics.Imaging.BitmapAlphaMode; +using BitmapPixelFormat = Windows.Graphics.Imaging.BitmapPixelFormat; +using SoftwareBitmap = Windows.Graphics.Imaging.SoftwareBitmap; +using WinBitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder; + +namespace Kniip.Editor; + +/// Tekst herkennen met de OCR die in Windows zit; de taal volgt de talen van het gebruikersprofiel. +internal static class OcrService +{ + /// Kleinere afbeeldingen worden vergroot: kleine schermtekst mist de OCR anders vaak. + private const int UpscaleBelowPixels = 2_000_000; + + /// Herkende regels, gescheiden door regeleinden; leeg als er geen tekst is gevonden. + /// Er is geen taal voor tekstherkenning geïnstalleerd. + public static async Task RecognizeAsync(BitmapSource image) + { + OcrEngine engine = CreateEngine(); + using SoftwareBitmap bitmap = await ToSoftwareBitmapAsync(Prepare(image)); + OcrResult result = await engine.RecognizeAsync(bitmap); + return Layout(result); + } + + /// + /// Regels opnieuw opbouwen uit de plek van elk woord. Windows geeft de tekst per blok terug; bij afwisselende + /// kleuren of opmaak valt één regel dan in stukken die ver uit elkaar in het resultaat staan. + /// + private static string Layout(OcrResult result) + { + var words = result.Lines + .SelectMany(line => line.Words) + .Select(word => (word.Text, Box: new Rect(word.BoundingRect.X, word.BoundingRect.Y, word.BoundingRect.Width, word.BoundingRect.Height))) + .Where(word => word.Text.Length > 0 && word.Box.Height > 0) + .OrderBy(word => word.Box.Top + word.Box.Height / 2) + .ToList(); + + var rows = new List(); + foreach ((string text, Rect box) in words) + { + double center = box.Top + box.Height / 2; + Row? row = rows.LastOrDefault(r => center >= r.Top && center <= r.Bottom + && Math.Abs(r.Center - center) <= Math.Min(r.Height, box.Height) * 0.6); + if (row == null) + { + row = new Row(); + rows.Add(row); + } + + row.Add(text, box); + } + + rows.Sort((a, b) => a.Center.CompareTo(b.Center)); + double lineHeight = rows.Count == 0 ? 0 : rows.Select(r => r.Height).OrderBy(h => h).ElementAt(rows.Count / 2); + var output = new StringBuilder(); + for (int i = 0; i < rows.Count; i++) + { + if (i > 0) + { + output.AppendLine(); + // Een duidelijk grotere witruimte dan een gewone regelafstand: een lege regel tussen alinea's. + if (rows[i].Top - rows[i - 1].Bottom > lineHeight * 0.9) + { + output.AppendLine(); + } + } + + output.Append(rows[i].Text); + } + + return output.ToString(); + } + + private sealed class Row + { + private readonly List<(string Text, Rect Box)> _words = new(); + + public double Top { get; private set; } = double.MaxValue; + + public double Bottom { get; private set; } = double.MinValue; + + public double Height => Bottom - Top; + + public double Center => (Top + Bottom) / 2; + + public void Add(string text, Rect box) + { + _words.Add((text, box)); + Top = Math.Min(Top, box.Top); + Bottom = Math.Max(Bottom, box.Bottom); + } + + public string Text => string.Join(" ", _words.OrderBy(w => w.Box.Left).Select(w => w.Text)); + } + + private static OcrEngine CreateEngine() + { + if (OcrEngine.TryCreateFromUserProfileLanguages() is { } engine) + { + return engine; + } + + var english = new Language("en-US"); + if (OcrEngine.IsLanguageSupported(english) && OcrEngine.TryCreateFromLanguage(english) is { } fallback) + { + return fallback; + } + + throw new InvalidOperationException( + "Windows heeft geen taal voor tekstherkenning. Voeg er een toe via Instellingen > Tijd en taal > Taal en regio: " + + "kies een taal en zet bij de taalopties Optische tekenherkenning aan."); + } + + /// Op wit leggen (transparant leest de OCR als zwart) en zo nodig vergroten of verkleinen. + private static BitmapSource Prepare(BitmapSource image) + { + int width = image.PixelWidth, height = image.PixelHeight; + long pixels = (long)width * height; + double scale = pixels < UpscaleBelowPixels / 4 ? 3 : pixels < UpscaleBelowPixels ? 2 : 1; + double max = OcrEngine.MaxImageDimension; + scale = Math.Min(scale, Math.Min(max / width, max / height)); + int w = Math.Max(1, (int)Math.Round(width * scale)); + int h = Math.Max(1, (int)Math.Round(height * scale)); + + var visual = new DrawingVisual(); + using (DrawingContext dc = visual.RenderOpen()) + { + var area = new Rect(0, 0, w, h); + dc.DrawRectangle(Brushes.White, null, area); + dc.DrawImage(image, area); + } + + RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.HighQuality); + var target = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32); + target.Render(visual); + target.Freeze(); + return target; + } + + private static async Task ToSoftwareBitmapAsync(BitmapSource image) + { + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + using var png = new MemoryStream(); + encoder.Save(png); + png.Position = 0; + + using var stream = png.AsRandomAccessStream(); + WinBitmapDecoder decoder = await WinBitmapDecoder.CreateAsync(stream); + return await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + } +} diff --git a/src/Kniip/Infrastructure/TrayIcon.cs b/src/Kniip/Infrastructure/TrayIcon.cs index 2c8428c..d850e3c 100644 --- a/src/Kniip/Infrastructure/TrayIcon.cs +++ b/src/Kniip/Infrastructure/TrayIcon.cs @@ -17,6 +17,7 @@ internal sealed class TrayIcon : IDisposable private readonly ToolStripMenuItem _pauseItem; private readonly ToolStripMenuItem _snipItem; private readonly ToolStripMenuItem _windowItem; + private readonly ToolStripMenuItem _ocrItem; private readonly ToolStripMenuItem _recordItem; private readonly ToolStripMenuItem _gifItem; private readonly ToolStripMenuItem _foldersItem; @@ -35,6 +36,7 @@ internal sealed class TrayIcon : IDisposable public event Action? OpenFromClipboardRequested; public event Action? SnipRequested; public event Action? WindowSnipRequested; + public event Action? TextRecognitionRequested; public event Action? RecordRequested; public event Action? GifRequested; public event Action? OpenFileRequested; @@ -62,6 +64,11 @@ internal sealed class TrayIcon : IDisposable menu.Items.Add(_snipItem); _windowItem = new ToolStripMenuItem("Venster knippen", null, (_, _) => WindowSnipRequested?.Invoke()); menu.Items.Add(_windowItem); + _ocrItem = new ToolStripMenuItem("OCR (tekst herkennen)...", null, (_, _) => TextRecognitionRequested?.Invoke()) + { + ToolTipText = "Sleep een rechthoek; de tekst daarin opent in een apart venster om te kopiëren" + }; + menu.Items.Add(_ocrItem); _delayItem = BuildDelayItem(); menu.Items.Add(_delayItem); _recordItem = new ToolStripMenuItem(RecordText, null, (_, _) => RecordRequested?.Invoke()); @@ -108,9 +115,10 @@ internal sealed class TrayIcon : IDisposable public bool IsPaused => _pauseItem.Checked; /// Toont de sneltoetsen naast de menu-items; null = geen sneltoets. - public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard) + public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard, string? ocr) { _clipboardItem.ShortcutKeyDisplayString = clipboard; + _ocrItem.ShortcutKeyDisplayString = ocr; _snipItem.ShortcutKeyDisplayString = snip; _windowItem.ShortcutKeyDisplayString = window; _recordItem.ShortcutKeyDisplayString = record; diff --git a/src/Kniip/Kniip.csproj b/src/Kniip/Kniip.csproj index 7b21ade..7ec54c1 100644 --- a/src/Kniip/Kniip.csproj +++ b/src/Kniip/Kniip.csproj @@ -18,7 +18,7 @@ app.manifest kniip Annotatie-editor naast het Windows Knipprogramma - 1.1.9 + 2.0.0 nl;en $(NoWarn);WFAC010 diff --git a/src/Kniip/Settings/AppSettings.cs b/src/Kniip/Settings/AppSettings.cs index 2bdbeca..ac9524d 100644 --- a/src/Kniip/Settings/AppSettings.cs +++ b/src/Kniip/Settings/AppSettings.cs @@ -88,6 +88,9 @@ public sealed class AppSettings /// Sneltoets om de afbeelding op het klembord in de editor te openen; standaard geen. public string ClipboardHotkey { get; set; } = ""; + + /// Sneltoets om een rechthoek te kiezen en de tekst erin te herkennen; standaard geen. + public string OcrHotkey { get; set; } = ""; public bool RecordSystemAudio { get; set; } = true; public bool RecordMicrophone { get; set; } public bool RecordShowCursor { get; set; } = true; diff --git a/src/Kniip/Settings/SettingsWindow.xaml b/src/Kniip/Settings/SettingsWindow.xaml index 74fe789..fc55a5b 100644 --- a/src/Kniip/Settings/SettingsWindow.xaml +++ b/src/Kniip/Settings/SettingsWindow.xaml @@ -131,13 +131,20 @@