This commit is contained in:
rogier committed 2026-11-10 23:54:21 +01:00
1 parent 1f3912ef57
commit 561ea57d3e
20 files changed
+820 -63

No files matched your search

+19
View File
@@ -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
+2 -2
View File
@@ -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
```
+1 -1
View File
@@ -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
)
+1 -1
View File
@@ -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"
+54 -4
View File
@@ -21,6 +21,9 @@ namespace Kniip;
public partial class App : Application
{
/// <summary>Waar een rechthoekknipsel heen gaat: naar de editor of naar tekstherkenning.</summary>
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<RegionCaptureWindow> _snipWindows = new();
private List<SnappedWindow> _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
/// <summary>Bevriest alle schermen (na de ingestelde vertraging) en laat een venster aanklikken; het resultaat opent in de editor.</summary>
public void BeginWindowSnip() => BeginDelayedSnip(pickWindow: true);
private void BeginDelayedSnip(bool pickWindow)
/// <summary>Als een rechthoekknipsel, maar de herkende tekst opent in een apart venster in plaats van de editor.</summary>
/// <param name="finished">Na het kiezen of annuleren, vóórdat het tekstvenster opent; bijvoorbeeld om de editor terug te zetten.</param>
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<SnappedWindow> 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));
}
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
+1 -1
View File
@@ -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));
+17 -8
View File
@@ -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 }
/// <summary>
/// Wat er onder het knipsel ligt; alleen zichtbaar waar het knipsel transparant is.
/// De kleuren en afbeelding blijven bewaard bij het wisselen van soort.
/// </summary>
/// <param name="Angle">Richting van een lineair verloop in graden, met de klok mee vanaf boven: 90 = naar rechts, 180 = naar onder.</param>
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);
}
/// <summary>Afbeelding zo schalen dat ze het hele vlak vult; wat uitsteekt valt weg.</summary>
private static Rect Cover(double imageWidth, double imageHeight, Rect area)
+1 -1
View File
@@ -7,7 +7,7 @@
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White" Geometry="M0,0 H2 V2 H0 Z" />
<GeometryDrawing Brush="#D4D4D4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
<GeometryDrawing Brush="#E4E4E4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
+11 -29
View File
@@ -288,7 +288,7 @@
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White" Geometry="M0,0 H2 V2 H0 Z" />
<GeometryDrawing Brush="#C8C8C8" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
<GeometryDrawing Brush="#E4E4E4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
@@ -326,32 +326,6 @@
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<!-- Richting van het verloop -->
<DataTemplate x:Key="GradientDirectionTemplate">
<StackPanel Orientation="Horizontal">
<Path x:Name="Icon" Data="M9,2.5 V15.5 M5,11.5 L9,15.5 L13,11.5" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Width="18" Height="18" VerticalAlignment="Center" />
<TextBlock x:Name="Label" Text="Van boven naar onder" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Horizontal}">
<Setter TargetName="Icon" Property="Data" Value="M2.5,9 H15.5 M11.5,5 L15.5,9 L11.5,13" />
<Setter TargetName="Label" Property="Text" Value="Van links naar rechts" />
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Diagonal}">
<Setter TargetName="Icon" Property="Data" Value="M3.5,3.5 L14.5,14.5 M8.5,14.5 H14.5 V8.5" />
<Setter TargetName="Label" Property="Text" Value="Diagonaal" />
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Radial}">
<Setter TargetName="Icon" Property="Data" Value="M9,3 A6,6 0 1 1 8.99,3 Z M9,7.5 A1.5,1.5 0 1 1 8.99,7.5 Z" />
<Setter TargetName="Label" Property="Text" Value="Vanuit het midden" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter TargetName="Label" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<DockPanel>
<!-- Statusbalk -->
@@ -466,6 +440,15 @@
<TextBlock x:Name="ScreencastLabel" Text="Screencast" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</Button>
<Button x:Name="OcrButton" local:ToolbarPanel.ItemId="OcrSnip" local:ToolbarPanel.Title="OCR"
Style="{StaticResource ProminentButton}" Click="Ocr_Click"
ToolTip="OCR: sleep een rechthoek op het scherm en kopieer de tekst daarin. Ctrl+Shift+T leest de tekst van dit knipsel.">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource IconPath}" StrokeThickness="1.2"
Data="M3,7 V5.5 A2.5,2.5 0 0 1 5.5,3 H7 M13,3 H14.5 A2.5,2.5 0 0 1 17,5.5 V7 M17,13 V14.5 A2.5,2.5 0 0 1 14.5,17 H13 M7,17 H5.5 A2.5,2.5 0 0 1 3,14.5 V13 M7,7.5 H13 M7,10 H13 M7,12.5 H10.5" />
<TextBlock x:Name="OcrLabel" Text="OCR" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</Button>
<!-- Wat tussen de twee spacers staat, komt in het midden van de werkbalk -->
<local:ToolbarSpacer local:ToolbarPanel.ItemId="Spacer2" />
@@ -724,8 +707,7 @@
ToolTip="Achtergrond onder de transparante delen" SelectionChanged="BackgroundKind_Changed" />
<local:ColorPickerButton x:Name="BackgroundColorPicker" ToolTip="Kleur" />
<local:ColorPickerButton x:Name="BackgroundColor2Picker" ToolTip="Tweede kleur van het verloop" />
<ComboBox x:Name="GradientDirectionCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource GradientDirectionTemplate}"
ToolTip="Richting van het verloop" SelectionChanged="GradientDirection_Changed" />
<local:GradientAngleDial x:Name="GradientDial" Margin="2,0" />
<Button x:Name="BackgroundImageButton" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30" Margin="2,0"
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}"
ToolTip="Afbeelding kiezen; die vult het hele canvas" Click="BackgroundImage_Click">
+71 -13
View File
@@ -46,28 +46,25 @@ public partial class EditorWindow : Window
private static readonly int[] CloudBubblesValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 15, 20 };
private static readonly BackgroundKind[] BackgroundKindValues =
{ BackgroundKind.None, BackgroundKind.Color, BackgroundKind.Gradient, BackgroundKind.Image };
private static readonly GradientDirection[] GradientDirectionValues =
{ GradientDirection.Vertical, GradientDirection.Horizontal, GradientDirection.Diagonal, GradientDirection.Radial };
/// <summary>Standaardvolgorde in de gesplitste indeling: eerst de bovenste werkbalk, dan de onderste.</summary>
private static readonly string[] DefaultSplitOrder =
{
"SplitSpacer1", "Select", "Sep1", "Rectangle", "Ellipse", "Shape", "Line", "Arrow", "Text", "Step", "Cursor", "Emoji", "Highlight", "Blur",
"Sep2", "Crop", "Sep3", "Undo", "Redo", "Delete", "Sep4", "Clear", "SplitSpacer2", "Settings",
"New", "Screencast", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
"New", "Screencast", "OcrSnip", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
};
/// <summary>Items die in de gesplitste indeling op de onderste werkbalk staan.</summary>
private static readonly HashSet<string> SplitBottomIds = new()
{
"New", "Screencast", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
"New", "Screencast", "OcrSnip", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
};
/// <summary>Items die alleen in de gesplitste indeling op een werkbalk staan.</summary>
private static readonly HashSet<string> SplitOnlyIds = new() { "SplitSpacer1", "SplitSpacer2" };
/// <summary>Items die in de drie-split-indeling in de zijbalk staan in plaats van de onderste werkbalk.</summary>
private static readonly HashSet<string> CaptureBarIds = new() { "New", "Screencast" };
private static readonly HashSet<string> CaptureBarIds = new() { "New", "Screencast", "OcrSnip" };
private readonly EditorDocument _doc;
private readonly UndoStack _undo = new();
@@ -76,6 +73,8 @@ public partial class EditorWindow : Window
private readonly List<string> _defaultToolbarOrder = new();
private readonly List<string> _defaultSplitOrder = new();
private readonly Dictionary<string, UIElement> _toolbarItems = new();
private CanvasBackground? _backgroundBeforePreview;
private bool _recognizing;
private EditorLayout _appliedLayout;
private ToolKind _tool;
@@ -141,7 +140,6 @@ public partial class EditorWindow : Window
CloudBubblesCombo.ItemsSource = CloudBubblesValues;
ShapeKindCombo.ItemsSource = ShapeKindValues;
BackgroundKindCombo.ItemsSource = BackgroundKindValues;
GradientDirectionCombo.ItemsSource = GradientDirectionValues;
UpdateShapeToolIcon();
StrokePicker.ColorChanged += c => ChangeStyle("stroke", s => s.Stroke = c);
@@ -149,6 +147,10 @@ public partial class EditorWindow : Window
ForegroundPicker.ColorChanged += c => ChangeStyle("foreground", s => s.Foreground = c);
BackgroundColorPicker.ColorChanged += c => ChangeBackground(b => b with { Color = c });
BackgroundColor2Picker.ColorChanged += c => ChangeBackground(b => b with { Color2 = c });
GradientDial.AngleChanging += PreviewGradientAngle;
GradientDial.AngleCommitted += angle => CommitGradient(b => b with { Direction = GradientDirection.Linear, Angle = angle });
GradientDial.RadialChanged += radial =>
CommitGradient(b => b with { Direction = radial ? GradientDirection.Radial : GradientDirection.Linear });
EmojiPicker.Recent = settings.RecentEmoji;
EmojiPicker.EmojiPicked += emoji => ChangeStyle("emoji", s => s.Emoji = emoji);
EmojiPicker.SkinToneChanged += tone => ChangeStyle("skintone", s => s.SkinTone = tone);
@@ -460,7 +462,7 @@ public partial class EditorWindow : Window
// Labels alleen tonen als alles op één regel past; staat de werkbalk opzij of zweeft hij, dan altijd alleen iconen.
private void UpdateActionLabels(Border bar, ToolbarPanel panel)
{
List<TextBlock> labels = new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, CopyLabel }
List<TextBlock> labels = new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, OcrLabel, CopyLabel }
.Where(label => PanelOf(label) == panel).ToList();
if (labels.Count == 0)
{
@@ -2094,7 +2096,7 @@ public partial class EditorWindow : Window
BackgroundKind kind = background.Kind;
SetVisible(BackgroundColorPicker, kind is BackgroundKind.Color or BackgroundKind.Gradient);
SetVisible(BackgroundColor2Picker, kind == BackgroundKind.Gradient);
SetVisible(GradientDirectionCombo, kind == BackgroundKind.Gradient);
SetVisible(GradientDial, kind == BackgroundKind.Gradient);
SetVisible(BackgroundImageButton, kind == BackgroundKind.Image);
BackgroundColorPicker.ToolTip = kind == BackgroundKind.Gradient ? "Eerste kleur van het verloop" : "Kleur";
BackgroundImageThumb.Source = background.Image;
@@ -2106,7 +2108,8 @@ public partial class EditorWindow : Window
try
{
BackgroundKindCombo.SelectedItem = kind;
GradientDirectionCombo.SelectedItem = background.Direction;
GradientDial.Angle = background.Angle;
GradientDial.IsRadial = background.Direction == GradientDirection.Radial;
BackgroundColorPicker.SelectedColor = background.Color;
BackgroundColor2Picker.SelectedColor = background.Color2;
}
@@ -2158,12 +2161,26 @@ public partial class EditorWindow : Window
ChangeBackground(b => b with { Kind = kind });
}
private void GradientDirection_Changed(object sender, SelectionChangedEventArgs e)
/// <summary>Live voorbeeld tijdens het slepen van de draaiknop; <see cref="CommitGradient"/> maakt er één stap van.</summary>
private void PreviewGradientAngle(double angle)
{
if (GradientDirectionCombo.SelectedItem is GradientDirection direction)
_backgroundBeforePreview ??= _doc.Background;
_doc.Background = _doc.Background with { Direction = GradientDirection.Linear, Angle = angle };
Surface.InvalidateVisual();
}
private void CommitGradient(Func<CanvasBackground, CanvasBackground> change)
{
if (_backgroundBeforePreview is { } before)
{
ChangeBackground(b => b with { Direction = direction });
_backgroundBeforePreview = null;
CanvasBackground updated = change(_doc.Background);
_doc.Background = before;
ChangeBackground(_ => updated);
return;
}
ChangeBackground(change);
}
private void BackgroundImage_Click(object sender, RoutedEventArgs e)
@@ -2527,6 +2544,44 @@ public partial class EditorWindow : Window
}
}
/// <summary>Tekst van het geopende knipsel herkennen (Ctrl+Shift+T).</summary>
private async void RecognizeText()
{
if (_recognizing)
{
return;
}
CommitTextEdit();
BitmapSource image = DocumentRenderer.Export(_doc);
_recognizing = true;
ShowTemporaryHint("Tekst herkennen...");
try
{
await OcrResultWindow.Open(image, this).Completion;
}
finally
{
_recognizing = false;
}
}
/// <summary>Editor wegzetten en een rechthoek op het scherm laten kiezen; daarna komt de editor terug met het tekstvenster erboven.</summary>
private void Ocr_Click(object sender, RoutedEventArgs e)
{
CommitTextEdit();
CancelDrag();
WindowState previous = WindowState == WindowState.Minimized ? WindowState.Normal : WindowState;
WindowState = WindowState.Minimized;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
timer.Tick += (_, _) =>
{
timer.Stop();
App.Instance.BeginTextSnip(() => WindowState = previous);
};
timer.Start();
}
private static string DefaultName => $"Knipsel {DateTime.Now:yyyy-MM-dd HHmmss}";
private bool Save()
@@ -2864,6 +2919,9 @@ public partial class EditorWindow : Window
case Key.U when CanUpload:
Upload();
break;
case Key.T when shift:
RecognizeText();
break;
case Key.N:
DiscardAndStartNewSnip();
break;
+64
View File
@@ -0,0 +1,64 @@
<UserControl x:Class="Kniip.Editor.GradientAngleDial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
VerticalAlignment="Center">
<Grid>
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}"
MouseWheel="Dial_MouseWheel">
<StackPanel Orientation="Horizontal">
<Grid Width="20" Height="20">
<Ellipse Stroke="{DynamicResource InkBrush}" StrokeThickness="1.2" Margin="1" />
<Path x:Name="SmallArrow" Data="M10,15 V5 M7,8 L10,5 L13,8" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<RotateTransform x:Name="SmallRotate" />
</Path.RenderTransform>
</Path>
<Ellipse x:Name="SmallRadial" Width="5" Height="5" Fill="{DynamicResource InkBrush}" Visibility="Collapsed" />
</Grid>
<TextBlock Text="&#xE70D;" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="{DynamicResource SubtleTextBrush}" />
</StackPanel>
</ToggleButton>
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
AllowsTransparency="True" PopupAnimation="Fade"
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
<Border Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="10" Margin="0,4,10,10">
<Border.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
</Border.Effect>
<StackPanel>
<!-- Sleep de knop rond; klik ergens op de cirkel om de richting meteen te kiezen -->
<Canvas x:Name="DialCanvas" Width="140" Height="140" Background="Transparent" Cursor="Hand"
MouseLeftButtonDown="Dial_MouseDown" MouseMove="Dial_MouseMove" MouseLeftButtonUp="Dial_MouseUp"
LostMouseCapture="Dial_LostMouseCapture" MouseWheel="Dial_MouseWheel">
<Ellipse x:Name="Track" Canvas.Left="20" Canvas.Top="20" Width="100" Height="100"
Stroke="{DynamicResource ControlStrongBorderBrush}" StrokeThickness="2" />
<Canvas x:Name="Ticks" IsHitTestVisible="False" />
<Line x:Name="Needle" X1="70" Y1="70" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
StrokeStartLineCap="Round" StrokeEndLineCap="Round" IsHitTestVisible="False" />
<Ellipse Canvas.Left="66" Canvas.Top="66" Width="8" Height="8" Fill="{DynamicResource AccentBrush}" IsHitTestVisible="False" />
<Ellipse x:Name="Knob" Width="16" Height="16" Fill="White" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
IsHitTestVisible="False" />
</Canvas>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
<TextBox x:Name="AngleBox" Width="48" VerticalContentAlignment="Center" HorizontalContentAlignment="Right" FontSize="12"
Padding="2,3" KeyDown="AngleBox_KeyDown" LostKeyboardFocus="AngleBox_LostKeyboardFocus"
ToolTip="Hoek in graden: 0 = naar boven, 90 = naar rechts, 180 = naar onder" />
<TextBlock Text="°" FontSize="12" VerticalAlignment="Center" Margin="3,0,0,0" Foreground="{DynamicResource InkBrush}" />
</StackPanel>
<ToggleButton x:Name="RadialToggle" Style="{StaticResource ToolToggle}" Margin="0,8,0,0" HorizontalAlignment="Stretch"
Click="Radial_Click" ToolTip="Verloop vanuit het midden naar buiten">
<StackPanel Orientation="Horizontal">
<Path Data="M9,3 A6,6 0 1 1 8.99,3 Z M9,7.5 A1.5,1.5 0 1 1 8.99,7.5 Z" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
Width="18" Height="18" VerticalAlignment="Center" />
<TextBlock Text="Vanuit het midden" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</ToggleButton>
<TextBlock Text="Shift = stappen van 15°, Alt = niet vastklikken" FontSize="11" Margin="0,6,0,0" HorizontalAlignment="Center"
Foreground="{DynamicResource SubtleTextBrush}" />
</StackPanel>
</Border>
</Popup>
</Grid>
</UserControl>
+250
View File
@@ -0,0 +1,250 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Kniip.Editor;
/// <summary>
/// Richting van een verloop: een cirkel met een knop die je rondsleept, plus een hoekveld en "vanuit het midden".
/// Hoeken in graden met de klok mee vanaf boven, zoals <see cref="CanvasBackground.Angle"/>.
/// </summary>
public partial class GradientAngleDial : UserControl
{
private const double Center = 70;
private const double Radius = 50;
private double _angle = 180;
private bool _isRadial;
private bool _dragging;
/// <summary>Tijdens het slepen, voor een live voorbeeld; nog geen stap in Ongedaan maken.</summary>
public event Action<double>? AngleChanging;
/// <summary>Definitieve hoek: bij loslaten, scrollen of typen. Kiest ook een lineair verloop.</summary>
public event Action<double>? AngleCommitted;
/// <summary>"Vanuit het midden" aan- of uitgezet.</summary>
public event Action<bool>? RadialChanged;
public GradientAngleDial()
{
InitializeComponent();
BuildTicks();
UpdateVisuals();
}
public double Angle
{
get => _angle;
set
{
_angle = Normalize(value);
UpdateVisuals();
}
}
public bool IsRadial
{
get => _isRadial;
set
{
_isRadial = value;
UpdateVisuals();
}
}
private static double Normalize(double angle)
{
angle %= 360;
if (angle < 0)
{
angle += 360;
}
return Math.Round(angle, 1) >= 360 ? 0 : Math.Round(angle, 1);
}
private static Vector Direction(double angle)
{
double radians = angle * Math.PI / 180;
return new Vector(Math.Sin(radians), -Math.Cos(radians));
}
private void BuildTicks()
{
for (int i = 0; i < 8; i++)
{
Vector d = Direction(i * 45);
double inner = Radius + 4, outer = Radius + (i % 2 == 0 ? 11 : 8);
Ticks.Children.Add(new Line
{
X1 = Center + d.X * inner,
Y1 = Center + d.Y * inner,
X2 = Center + d.X * outer,
Y2 = Center + d.Y * outer,
StrokeThickness = 1.5,
StrokeStartLineCap = PenLineCap.Round,
StrokeEndLineCap = PenLineCap.Round
});
((Line)Ticks.Children[i]).SetResourceReference(Shape.StrokeProperty, "SubtleTextBrush");
}
}
private void UpdateVisuals()
{
Vector d = Direction(_angle);
Point knob = new Point(Center, Center) + d * Radius;
Canvas.SetLeft(Knob, knob.X - Knob.Width / 2);
Canvas.SetTop(Knob, knob.Y - Knob.Height / 2);
Needle.X2 = knob.X;
Needle.Y2 = knob.Y;
DialCanvas.Opacity = _isRadial ? 0.45 : 1;
SmallRotate.Angle = _angle;
SmallArrow.Visibility = _isRadial ? Visibility.Collapsed : Visibility.Visible;
SmallRadial.Visibility = _isRadial ? Visibility.Visible : Visibility.Collapsed;
RadialToggle.IsChecked = _isRadial;
if (!AngleBox.IsKeyboardFocusWithin)
{
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
}
Toggle.ToolTip = _isRadial
? "Richting van het verloop: vanuit het midden"
: $"Richting van het verloop: {_angle.ToString("0.#", CultureInfo.CurrentCulture)}° (scroll om te draaien)";
}
private double AngleAt(Point p)
{
Vector v = p - new Point(Center, Center);
if (v.Length < 1)
{
return _angle;
}
double angle = Normalize(Math.Atan2(v.X, -v.Y) * 180 / Math.PI);
ModifierKeys modifiers = Keyboard.Modifiers;
if (modifiers.HasFlag(ModifierKeys.Alt))
{
return Normalize(Math.Round(angle));
}
if (modifiers.HasFlag(ModifierKeys.Shift))
{
return Normalize(Math.Round(angle / 15) * 15);
}
double nearest = Math.Round(angle / 45) * 45;
return Normalize(Math.Abs(angle - nearest) <= 6 ? nearest : Math.Round(angle));
}
private void Dial_MouseDown(object sender, MouseButtonEventArgs e)
{
double angle = AngleAt(e.GetPosition(DialCanvas));
_dragging = DialCanvas.CaptureMouse();
if (_dragging)
{
Preview(angle);
}
else
{
Commit(angle);
}
e.Handled = true;
}
private void Dial_MouseMove(object sender, MouseEventArgs e)
{
if (_dragging)
{
Preview(AngleAt(e.GetPosition(DialCanvas)));
}
}
private void Dial_MouseUp(object sender, MouseButtonEventArgs e)
{
if (_dragging)
{
DialCanvas.ReleaseMouseCapture();
}
}
private void Dial_LostMouseCapture(object sender, MouseEventArgs e)
{
if (!_dragging)
{
return;
}
_dragging = false;
Commit(_angle);
}
private void Dial_MouseWheel(object sender, MouseWheelEventArgs e)
{
double step = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt) ? 1 : 15;
double angle = e.Delta > 0 ? _angle + step : _angle - step;
Commit(step == 15 ? Math.Round(angle / 15) * 15 : angle);
e.Handled = true;
}
private void Preview(double angle)
{
_isRadial = false;
Angle = angle;
AngleChanging?.Invoke(_angle);
}
private void Commit(double angle)
{
_isRadial = false;
Angle = angle;
AngleCommitted?.Invoke(_angle);
}
private void AngleBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
ApplyTypedAngle();
AngleBox.SelectAll();
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
Toggle.IsChecked = false;
e.Handled = true;
}
}
private void AngleBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) => ApplyTypedAngle();
private void ApplyTypedAngle()
{
string text = AngleBox.Text.Trim().TrimEnd('°');
if (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out double angle)
|| double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out angle))
{
if (Normalize(angle) != _angle || _isRadial)
{
Commit(angle);
return;
}
}
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
}
private void Radial_Click(object sender, RoutedEventArgs e)
{
_isRadial = RadialToggle.IsChecked == true;
UpdateVisuals();
RadialChanged?.Invoke(_isRadial);
}
}
+31
View File
@@ -0,0 +1,31 @@
<Window x:Class="Kniip.Editor.OcrResultWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="kniip tekst herkennen" Width="560" Height="420" MinWidth="360" MinHeight="240"
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
<DockPanel Margin="14">
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0" LastChildFill="False">
<TextBlock x:Name="StatusText" DockPanel.Dock="Left" VerticalAlignment="Center" FontSize="12"
Foreground="{DynamicResource SubtleTextBrush}" TextTrimming="CharacterEllipsis" />
<Button DockPanel.Dock="Right" Style="{StaticResource SecondaryButton}" Content="Sluiten" IsCancel="True" Click="Close_Click" />
<Button x:Name="CopyButton" DockPanel.Dock="Right" Style="{StaticResource AccentButton}" ToolTip="Alles kopiëren, of alleen de geselecteerde tekst"
Click="Copy_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8C8;" Style="{StaticResource IconText}" FontSize="14" Foreground="White" />
<TextBlock x:Name="CopyLabel" Text="Kopiëren" Margin="6,0,0,0" />
</StackPanel>
</Button>
</DockPanel>
<Grid>
<TextBox x:Name="ResultBox" AcceptsReturn="True" AcceptsTab="True" TextWrapping="Wrap" Padding="6"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
FontFamily="Segoe UI" FontSize="13" SelectionChanged="ResultBox_SelectionChanged" />
<StackPanel x:Name="EmptyPanel" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" IsHitTestVisible="False">
<TextBlock Text="&#xE8D2;" Style="{StaticResource IconText}" FontSize="28" Foreground="{DynamicResource SubtleTextBrush}" />
<TextBlock x:Name="EmptyText" Text="Geen tekst gevonden in deze afbeelding." Margin="0,8,0,0" TextWrapping="Wrap"
TextAlignment="Center" MaxWidth="380" Foreground="{DynamicResource SubtleTextBrush}" />
</StackPanel>
</Grid>
</DockPanel>
</Window>
+110
View File
@@ -0,0 +1,110 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
namespace Kniip.Editor;
/// <summary>Toont de herkende tekst van een afbeelding, om aan te passen en te kopiëren.</summary>
public partial class OcrResultWindow : Window
{
private OcrResultWindow()
{
InitializeComponent();
Infrastructure.ThemeManager.Attach(this);
}
/// <summary>Klaar zodra de tekst herkend is of het herkennen is mislukt.</summary>
public Task Completion { get; private set; } = Task.CompletedTask;
/// <summary>Opent het venster meteen en vult het zodra de tekst herkend is.</summary>
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();
}
+162
View File
@@ -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;
/// <summary>Tekst herkennen met de OCR die in Windows zit; de taal volgt de talen van het gebruikersprofiel.</summary>
internal static class OcrService
{
/// <summary>Kleinere afbeeldingen worden vergroot: kleine schermtekst mist de OCR anders vaak.</summary>
private const int UpscaleBelowPixels = 2_000_000;
/// <summary>Herkende regels, gescheiden door regeleinden; leeg als er geen tekst is gevonden.</summary>
/// <exception cref="InvalidOperationException">Er is geen taal voor tekstherkenning geïnstalleerd.</exception>
public static async Task<string> RecognizeAsync(BitmapSource image)
{
OcrEngine engine = CreateEngine();
using SoftwareBitmap bitmap = await ToSoftwareBitmapAsync(Prepare(image));
OcrResult result = await engine.RecognizeAsync(bitmap);
return Layout(result);
}
/// <summary>
/// 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.
/// </summary>
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<Row>();
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.");
}
/// <summary>Op wit leggen (transparant leest de OCR als zwart) en zo nodig vergroten of verkleinen.</summary>
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<SoftwareBitmap> 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);
}
}
+9 -1
View File
@@ -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;
/// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary>
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;
+1 -1
View File
@@ -18,7 +18,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<Product>kniip</Product>
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
<Version>1.1.9</Version>
<Version>2.0.0</Version>
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
<NoWarn>$(NoWarn);WFAC010</NoWarn>
+3
View File
@@ -88,6 +88,9 @@ public sealed class AppSettings
/// <summary>Sneltoets om de afbeelding op het klembord in de editor te openen; standaard geen.</summary>
public string ClipboardHotkey { get; set; } = "";
/// <summary>Sneltoets om een rechthoek te kiezen en de tekst erin te herkennen; standaard geen.</summary>
public string OcrHotkey { get; set; } = "";
public bool RecordSystemAudio { get; set; } = true;
public bool RecordMicrophone { get; set; }
public bool RecordShowCursor { get; set; } = true;
+8 -1
View File
@@ -131,13 +131,20 @@
<Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="Vanaf klembord" VerticalAlignment="Center" Width="150" />
<Button x:Name="ClipboardHotkeyButton" Tag="Clipboard" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; opent de afbeelding op het klembord" />
<Button Tag="Clipboard" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="OCR (tekst herkennen)" VerticalAlignment="Center" Width="150" />
<Button x:Name="OcrHotkeyButton" Tag="Ocr" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; sleep daarna een rechthoek om de tekst erin te lezen" />
<Button Tag="Ocr" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" />
</StackPanel>
<TextBlock x:Name="HotkeyHint" Style="{StaticResource Hint}" Margin="0,0,0,4" />
<TextBlock Text="Gebied kiezen" Style="{StaticResource CardTitle}" Margin="0,12,0,8" />
<CheckBox x:Name="MagnifierBox" Content="Vergrootglas tonen bij het kiezen van een gebied (knipsel en screencast)" />
@@ -27,6 +27,7 @@ public partial class SettingsWindow : Window
["Record"] = "screencast (MP4)",
["Gif"] = "screencast (GIF)",
["Clipboard"] = "bewerken vanaf klembord",
["Ocr"] = "OCR",
};
private readonly AppSettings _settings;
@@ -66,6 +67,7 @@ public partial class SettingsWindow : Window
["Record"] = RecordHotkeyButton,
["Gif"] = GifHotkeyButton,
["Clipboard"] = ClipboardHotkeyButton,
["Ocr"] = OcrHotkeyButton,
};
HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true
@@ -127,6 +129,7 @@ public partial class SettingsWindow : Window
_hotkeys["Record"] = settings.RecordHotkey;
_hotkeys["Gif"] = settings.GifHotkey;
_hotkeys["Clipboard"] = settings.ClipboardHotkey;
_hotkeys["Ocr"] = settings.OcrHotkey;
foreach (string id in _hotkeys.Keys)
{
ShowHotkey(id);
@@ -477,6 +480,7 @@ public partial class SettingsWindow : Window
_settings.RecordHotkey = _hotkeys["Record"];
_settings.GifHotkey = _hotkeys["Gif"];
_settings.ClipboardHotkey = _hotkeys["Clipboard"];
_settings.OcrHotkey = _hotkeys["Ocr"];
_settings.RecordSystemAudio = SystemAudioBox.IsChecked == true;
_settings.RecordMicrophone = MicrophoneBox.IsChecked == true;
_settings.RecordShowCursor = ShowCursorBox.IsChecked == true;