Add scanning QR code from image

pull/5855/head
2dust 2024-10-18 17:35:32 +08:00
parent b74ddc0b43
commit 5c0fba8744
16 changed files with 208 additions and 89 deletions

View File

@ -1,4 +1,6 @@
using QRCoder; using QRCoder;
using SkiaSharp;
using ZXing.SkiaSharp;
namespace ServiceLib.Common namespace ServiceLib.Common
{ {
@ -11,5 +13,78 @@ namespace ServiceLib.Common
using PngByteQRCode qrCode = new(qrCodeData); using PngByteQRCode qrCode = new(qrCodeData);
return qrCode.GetGraphic(20); return qrCode.GetGraphic(20);
} }
public static string? ParseBarcode(string? fileName)
{
if (fileName == null || !File.Exists(fileName))
{
return null;
}
try
{
var image = SKImage.FromEncodedData(fileName);
var bitmap = SKBitmap.FromImage(image);
return ReaderBarcode(bitmap);
}
catch
{
// ignored
}
return null;
}
public static string? ParseBarcode(byte[]? bytes)
{
try
{
var bitmap = SKBitmap.Decode(bytes);
//using var stream = new FileStream("test2.png", FileMode.Create, FileAccess.Write);
//using var image = SKImage.FromBitmap(bitmap);
//using var encodedImage = image.Encode();
//encodedImage.SaveTo(stream);
return ReaderBarcode(bitmap);
}
catch
{
// ignored
}
return null;
}
private static string? ReaderBarcode(SKBitmap? bitmap)
{
var reader = new BarcodeReader();
var result = reader.Decode(bitmap);
if (result != null && Utils.IsNotEmpty(result.Text))
{
return result.Text;
}
//FlipBitmap
var result2 = reader.Decode(FlipBitmap(bitmap));
return result2?.Text;
}
private static SKBitmap FlipBitmap(SKBitmap bmp)
{
// Create a bitmap (to return)
var flipped = new SKBitmap(bmp.Width, bmp.Height, bmp.Info.ColorType, bmp.Info.AlphaType);
// Create a canvas to draw into the bitmap
using var canvas = new SKCanvas(flipped);
// Set a transform matrix which moves the bitmap to the right,
// and then "scales" it by -1, which just flips the pixels
// horizontally
canvas.Translate(bmp.Width, 0);
canvas.Scale(-1, 1);
canvas.DrawBitmap(bmp, 0, 0);
return flipped;
}
} }
} }

View File

@ -15,6 +15,7 @@
ShareServer, ShareServer,
ShowHideWindow, ShowHideWindow,
ScanScreenTask, ScanScreenTask,
ScanImageTask,
Shutdown, Shutdown,
BrowseServer, BrowseServer,
ImportRulesFromFile, ImportRulesFromFile,

View File

@ -681,6 +681,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Scan QR code in the image 的本地化字符串。
/// </summary>
public static string menuAddServerViaImage {
get {
return ResourceManager.GetString("menuAddServerViaImage", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Scan QR code on the screen (Ctrl+S) 的本地化字符串。 /// 查找类似 Scan QR code on the screen (Ctrl+S) 的本地化字符串。
/// </summary> /// </summary>

View File

@ -1351,4 +1351,7 @@
<data name="TbSettingsChinaUserTip" xml:space="preserve"> <data name="TbSettingsChinaUserTip" xml:space="preserve">
<value>Users in China region can ignore this item</value> <value>Users in China region can ignore this item</value>
</data> </data>
<data name="menuAddServerViaImage" xml:space="preserve">
<value>Scan QR code in the image</value>
</data>
</root> </root>

View File

@ -1348,4 +1348,7 @@
<data name="menuRegionalPresetsRussia" xml:space="preserve"> <data name="menuRegionalPresetsRussia" xml:space="preserve">
<value>俄罗斯</value> <value>俄罗斯</value>
</data> </data>
<data name="menuAddServerViaImage" xml:space="preserve">
<value>扫描图片中的二维码</value>
</data>
</root> </root>

View File

@ -1228,4 +1228,7 @@
<data name="menuRegionalPresetsRussia" xml:space="preserve"> <data name="menuRegionalPresetsRussia" xml:space="preserve">
<value>俄羅斯</value> <value>俄羅斯</value>
</data> </data>
<data name="menuAddServerViaImage" xml:space="preserve">
<value>掃描圖片中的二維碼</value>
</data>
</root> </root>

View File

@ -16,7 +16,9 @@
<PackageReference Include="WebDav.Client" Version="2.8.0" /> <PackageReference Include="WebDav.Client" Version="2.8.0" />
<PackageReference Include="YamlDotNet" Version="16.1.3" /> <PackageReference Include="YamlDotNet" Version="16.1.3" />
<PackageReference Include="QRCoder" Version="1.6.0" /> <PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="CliWrap" Version="3.6.6" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="SkiaSharp.QrCode" Version="0.7.0" />
<PackageReference Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -25,6 +25,7 @@ namespace ServiceLib.ViewModels
public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; } public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; } public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; } public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaImageCmd { get; }
//Subscription //Subscription
public ReactiveCommand<Unit, Unit> SubSettingCmd { get; } public ReactiveCommand<Unit, Unit> SubSettingCmd { get; }
@ -46,6 +47,7 @@ namespace ServiceLib.ViewModels
//Presets //Presets
public ReactiveCommand<Unit, Unit> RegionalPresetDefaultCmd { get; } public ReactiveCommand<Unit, Unit> RegionalPresetDefaultCmd { get; }
public ReactiveCommand<Unit, Unit> RegionalPresetRussiaCmd { get; } public ReactiveCommand<Unit, Unit> RegionalPresetRussiaCmd { get; }
public ReactiveCommand<Unit, Unit> ReloadCmd { get; } public ReactiveCommand<Unit, Unit> ReloadCmd { get; }
@ -121,7 +123,11 @@ namespace ServiceLib.ViewModels
}); });
AddServerViaScanCmd = ReactiveCommand.CreateFromTask(async () => AddServerViaScanCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await AddServerViaScanTaskAsync(); await AddServerViaScanAsync();
});
AddServerViaImageCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerViaImageAsync();
}); });
//Subscription //Subscription
@ -386,12 +392,34 @@ namespace ServiceLib.ViewModels
} }
} }
public async Task AddServerViaScanTaskAsync() public async Task AddServerViaScanAsync()
{ {
_updateView?.Invoke(EViewAction.ScanScreenTask, null); _updateView?.Invoke(EViewAction.ScanScreenTask, null);
} }
public void ScanScreenResult(string result) public async Task ScanScreenResult(byte[]? bytes)
{
var result = QRCodeHelper.ParseBarcode(bytes);
await AddScanResultAsync(result);
}
public async Task AddServerViaImageAsync()
{
_updateView?.Invoke(EViewAction.ScanImageTask, null);
}
public async Task ScanImageResult(string fileName)
{
if (Utils.IsNullOrEmpty(fileName))
{
return;
}
var result = QRCodeHelper.ParseBarcode(fileName);
await AddScanResultAsync(result);
}
private async Task AddScanResultAsync(string? result)
{ {
if (Utils.IsNullOrEmpty(result)) if (Utils.IsNullOrEmpty(result))
{ {
@ -571,6 +599,6 @@ namespace ServiceLib.ViewModels
Reload(); Reload();
} }
#endregion #endregion Presets
} }
} }

View File

@ -211,7 +211,7 @@ namespace ServiceLib.ViewModels
private async Task AddServerViaScan() private async Task AddServerViaScan()
{ {
var service = Locator.Current.GetService<MainWindowViewModel>(); var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null) await service.AddServerViaScanTaskAsync(); if (service != null) await service.AddServerViaScanAsync();
} }
private async Task UpdateSubscriptionProcess(bool blProxy) private async Task UpdateSubscriptionProcess(bool blProxy)

View File

@ -32,10 +32,8 @@
</StackPanel> </StackPanel>
</MenuItem.Header> </MenuItem.Header>
<MenuItem x:Name="menuAddServerViaClipboard" Header="{x:Static resx:ResUI.menuAddServerViaClipboard}" /> <MenuItem x:Name="menuAddServerViaClipboard" Header="{x:Static resx:ResUI.menuAddServerViaClipboard}" />
<MenuItem <MenuItem x:Name="menuAddServerViaScan" Header="{x:Static resx:ResUI.menuAddServerViaScan}" />
x:Name="menuAddServerViaScan" <MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" />
Header="{x:Static resx:ResUI.menuAddServerViaScan}"
IsVisible="False" />
<MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" /> <MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" />
<Separator /> <Separator />
<MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" /> <MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" />

View File

@ -4,6 +4,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading; using Avalonia.Threading;
using DialogHostAvalonia; using DialogHostAvalonia;
@ -60,6 +61,7 @@ namespace v2rayN.Desktop.Views
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables);
//sub //sub
this.BindCommand(ViewModel, vm => vm.SubSettingCmd, v => v.menuSubSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SubSettingCmd, v => v.menuSubSetting).DisposeWith(disposables);
@ -224,6 +226,10 @@ namespace v2rayN.Desktop.Views
await ScanScreenTaskAsync(); await ScanScreenTaskAsync();
break; break;
case EViewAction.ScanImageTask:
await ScanImageTaskAsync();
break;
case EViewAction.AddServerViaClipboard: case EViewAction.AddServerViaClipboard:
var clipboardData = await AvaUtils.GetClipboardData(this); var clipboardData = await AvaUtils.GetClipboardData(this);
ViewModel?.AddServerViaClipboardAsync(clipboardData); ViewModel?.AddServerViaClipboardAsync(clipboardData);
@ -324,7 +330,16 @@ namespace v2rayN.Desktop.Views
ShowHideWindow(true); ShowHideWindow(true);
//ViewModel?.ScanScreenTaskAsync(result); //ViewModel?.ScanScreenResult(result);
}
private async Task ScanImageTaskAsync()
{
var fileName = await UI.OpenFileDialog(this,null );
if (fileName.IsNullOrEmpty())
{
return;
}
await ViewModel?.ScanImageResult(fileName);
} }
private void MenuCheckUpdate_Click(object? sender, RoutedEventArgs e) private void MenuCheckUpdate_Click(object? sender, RoutedEventArgs e)

View File

@ -17,8 +17,7 @@
<Image <Image
Name="imgQrcode" Name="imgQrcode"
Width="300" Width="300"
Height="300" Height="300" />
Source="/Assets/close.png" />
<TextBox <TextBox
x:Name="txtContent" x:Name="txtContent"
@ -29,6 +28,6 @@
IsReadOnly="True" IsReadOnly="True"
MaxLines="1" /> MaxLines="1" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -4,10 +4,6 @@ using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using ZXing;
using ZXing.Common;
using ZXing.QrCode;
using ZXing.Windows.Compatibility;
namespace v2rayN namespace v2rayN
{ {
@ -25,11 +21,7 @@ namespace v2rayN
try try
{ {
var qrCodeImage = ServiceLib.Common.QRCodeHelper.GenQRCode(strContent); var qrCodeImage = ServiceLib.Common.QRCodeHelper.GenQRCode(strContent);
if (qrCodeImage is null) return qrCodeImage is null ? null : ByteToImage(qrCodeImage);
{
return null;
}
return ByteToImage(qrCodeImage);
} }
catch catch
{ {
@ -37,82 +29,54 @@ namespace v2rayN
} }
} }
private static ImageSource ByteToImage(byte[] imageData) public static byte[]? CaptureScreen(Window window)
{
BitmapImage biImg = new();
MemoryStream ms = new(imageData);
biImg.BeginInit();
biImg.StreamSource = ms;
biImg.EndInit();
ImageSource imgSrc = biImg as ImageSource;
return imgSrc;
}
public static string ScanScreen(float dpiX, float dpiY)
{ {
try try
{ {
GetDpi(window, out var dpiX, out var dpiY);
var left = (int)(SystemParameters.WorkArea.Left); var left = (int)(SystemParameters.WorkArea.Left);
var top = (int)(SystemParameters.WorkArea.Top); var top = (int)(SystemParameters.WorkArea.Top);
var width = (int)(SystemParameters.WorkArea.Width / dpiX); var width = (int)(SystemParameters.WorkArea.Width / dpiX);
var height = (int)(SystemParameters.WorkArea.Height / dpiY); var height = (int)(SystemParameters.WorkArea.Height / dpiY);
using Bitmap fullImage = new Bitmap(width, height); using var fullImage = new Bitmap(width, height);
using (Graphics g = Graphics.FromImage(fullImage)) using var g = Graphics.FromImage(fullImage);
{
g.CopyFromScreen(left, top, 0, 0, fullImage.Size, CopyPixelOperation.SourceCopy);
}
int maxTry = 10;
for (int i = 0; i < maxTry; i++)
{
int marginLeft = (int)((double)fullImage.Width * i / 2.5 / maxTry);
int marginTop = (int)((double)fullImage.Height * i / 2.5 / maxTry);
Rectangle cropRect = new(marginLeft, marginTop, fullImage.Width - marginLeft * 2, fullImage.Height - marginTop * 2);
Bitmap target = new(width, height);
double imageScale = (double)width / (double)cropRect.Width; g.CopyFromScreen(left, top, 0, 0, fullImage.Size, CopyPixelOperation.SourceCopy);
using (Graphics g = Graphics.FromImage(target)) //fullImage.Save("test1.png", ImageFormat.Png);
{ return ImageToByte(fullImage);
g.DrawImage(fullImage, new Rectangle(0, 0, target.Width, target.Height),
cropRect,
GraphicsUnit.Pixel);
}
BitmapLuminanceSource source = new(target);
QRCodeReader reader = new();
BinaryBitmap bitmap = new(new HybridBinarizer(source));
var result = reader.decode(bitmap);
if (result != null)
{
return result.Text;
}
else
{
BinaryBitmap bitmap2 = new(new HybridBinarizer(source.invert()));
var result2 = reader.decode(bitmap2);
if (result2 != null)
{
return result2.Text;
}
}
}
} }
catch (Exception ex) catch
{ {
Logging.SaveLog(ex.Message, ex); return null;
} }
return string.Empty;
} }
public static Tuple<float, float> GetDpiXY(Window window) private static void GetDpi(Window window, out float x, out float y)
{ {
IntPtr hWnd = new WindowInteropHelper(window).EnsureHandle(); var hWnd = new WindowInteropHelper(window).EnsureHandle();
Graphics g = Graphics.FromHwnd(hWnd); var g = Graphics.FromHwnd(hWnd);
return new(96 / g.DpiX, 96 / g.DpiY); x = 96 / g.DpiX;
y = 96 / g.DpiY;
}
private static ImageSource ByteToImage(byte[] imageData)
{
BitmapImage biImg = new();
using MemoryStream ms = new(imageData);
biImg.BeginInit();
biImg.StreamSource = ms;
biImg.EndInit();
return biImg as ImageSource;
}
private static byte[]? ImageToByte(Image img)
{
var converter = new ImageConverter();
return converter.ConvertTo(img, typeof(byte[])) as byte[];
} }
} }
} }

View File

@ -64,6 +64,10 @@
x:Name="menuAddServerViaScan" x:Name="menuAddServerViaScan"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddServerViaScan}" /> Header="{x:Static resx:ResUI.menuAddServerViaScan}" />
<MenuItem
x:Name="menuAddServerViaImage"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddServerViaImage}" />
<MenuItem <MenuItem
x:Name="menuAddCustomServer" x:Name="menuAddCustomServer"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"

View File

@ -82,6 +82,7 @@ namespace v2rayN.Views
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables);
//sub //sub
this.BindCommand(ViewModel, vm => vm.SubSettingCmd, v => v.menuSubSetting).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SubSettingCmd, v => v.menuSubSetting).DisposeWith(disposables);
@ -221,6 +222,10 @@ namespace v2rayN.Views
await ScanScreenTaskAsync(); await ScanScreenTaskAsync();
break; break;
case EViewAction.ScanImageTask:
await ScanImageTaskAsync();
break;
case EViewAction.AddServerViaClipboard: case EViewAction.AddServerViaClipboard:
var clipboardData = WindowsUtils.GetClipboardData(); var clipboardData = WindowsUtils.GetClipboardData();
ViewModel?.AddServerViaClipboardAsync(clipboardData); ViewModel?.AddServerViaClipboardAsync(clipboardData);
@ -317,15 +322,26 @@ namespace v2rayN.Views
{ {
ShowHideWindow(false); ShowHideWindow(false);
var dpiXY = QRCodeHelper.GetDpiXY(Application.Current.MainWindow); if (Application.Current?.MainWindow is Window window)
string result = await Task.Run(() =>
{ {
return QRCodeHelper.ScanScreen(dpiXY.Item1, dpiXY.Item2); var bytes = QRCodeHelper.CaptureScreen(window);
}); await ViewModel?.ScanScreenResult(bytes);
}
ShowHideWindow(true); ShowHideWindow(true);
}
ViewModel?.ScanScreenResult(result); private async Task ScanImageTaskAsync()
{
if (UI.OpenFileDialog(out var fileName, "PNG|*.png|All|*.*") != true)
{
return;
}
if (fileName.IsNullOrEmpty())
{
return;
}
await ViewModel?.ScanImageResult(fileName);
} }
private void MenuCheckUpdate_Click(object sender, RoutedEventArgs e) private void MenuCheckUpdate_Click(object sender, RoutedEventArgs e)

View File

@ -17,7 +17,6 @@
<PackageReference Include="MaterialDesignThemes" Version="5.1.0" /> <PackageReference Include="MaterialDesignThemes" Version="5.1.0" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.3" /> <PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.3" />
<PackageReference Include="TaskScheduler" Version="2.11.0" /> <PackageReference Include="TaskScheduler" Version="2.11.0" />
<PackageReference Include="ZXing.Net.Bindings.Windows.Compatibility" Version="0.16.12" />
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageReference Include="ReactiveUI.WPF" Version="20.1.63" /> <PackageReference Include="ReactiveUI.WPF" Version="20.1.63" />
</ItemGroup> </ItemGroup>