Initial implementation
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
using Ocr.Tesseract.Interface;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ocr.Tesseract.Screenshots.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the <see cref="ScreenshotProcessor"/>
|
||||
/// </summary>
|
||||
public class ScreenshotProcessorConfiguration : IImageProcessorConfiguration, INotifyPropertyChanged
|
||||
{
|
||||
#region Implementation of IMagickImageValueProcessorSettings
|
||||
|
||||
private int _border = 10;
|
||||
private bool _enableResizing = true;
|
||||
private bool _enableThresholding = true;
|
||||
private bool _filterConnectedComponents;
|
||||
private int _thresholdWidth = 15, _thresholdHeight = 17;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Border
|
||||
{
|
||||
get => _border;
|
||||
set
|
||||
{
|
||||
if (value == _border)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_border = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EnableResizing
|
||||
{
|
||||
get => _enableResizing;
|
||||
set
|
||||
{
|
||||
if (value == _enableResizing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enableResizing = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EnableThresholding
|
||||
{
|
||||
get => _enableThresholding;
|
||||
set
|
||||
{
|
||||
if (value == _enableThresholding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enableThresholding = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool FilterConnectedComponents
|
||||
{
|
||||
get => _filterConnectedComponents;
|
||||
set
|
||||
{
|
||||
if (value == _filterConnectedComponents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_filterConnectedComponents = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int ThresholdHeight
|
||||
{
|
||||
get => _thresholdHeight;
|
||||
set
|
||||
{
|
||||
if (value == _thresholdHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_thresholdHeight = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int ThresholdWidth
|
||||
{
|
||||
get => _thresholdWidth;
|
||||
set
|
||||
{
|
||||
if (value == _thresholdWidth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_thresholdWidth = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Implementation of IMagickImageValueProcessorSettings
|
||||
|
||||
#region Implementation of INotifyPropertyChanged
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
#endregion Implementation of INotifyPropertyChanged
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Ocr.Tesseract.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ocr.Tesseract.Screenshots.Configuration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class TesseractScreenshotConfiguration : ITesseractConfiguration
|
||||
{
|
||||
#region Implementation of ITesseractConfiguration
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DataPath { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string[] Languages { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object> Variables { get; set; } = new Dictionary<string, object>
|
||||
{
|
||||
{ "load_system_dawg", false },
|
||||
{ "language_model_penalty_non_freq_dict_word", 1 },
|
||||
{ "language_model_penalty_non_dict_word", 1 },
|
||||
// { "user_words_suffix", "fra.user-words" },
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<IncludeSymbols>True</IncludeSymbols>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.2.0" />
|
||||
<PackageReference Include="Magick.NET.Core" Version="13.2.0" />
|
||||
<PackageReference Include="Magick.NET.SystemDrawing" Version="7.0.6" />
|
||||
<PackageReference Include="Tesseract" Version="5.2.0" />
|
||||
<PackageReference Include="Tesseract.Drawing" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Lookup\Lookup.Interface\Lookup.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Process\Process.Abstract\Process.Abstract.csproj" />
|
||||
<ProjectReference Include="..\..\Process\Process.Interface\Process.Interface.csproj" />
|
||||
<ProjectReference Include="..\Ocr.Tesseract\Ocr.Tesseract.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,83 @@
|
||||
using ImageMagick;
|
||||
using Ocr.Tesseract.Extensions;
|
||||
using Ocr.Tesseract.Screenshots.Configuration;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract.Screenshots;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ScreenshotProcessor : ImageProcessor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ScreenshotProcessor(ScreenshotProcessorConfiguration configuration)
|
||||
: base(configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="System.Diagnostics.Process"/>
|
||||
public override IEnumerable<MagickImage> Process(MagickImage image)
|
||||
{
|
||||
var tImage = image.CloneImage();
|
||||
|
||||
if (Configuration.EnableResizing)
|
||||
{
|
||||
tImage = tImage
|
||||
.ResizeImage(
|
||||
2f,
|
||||
FilterType.Lanczos2Sharp,
|
||||
PixelInterpolateMethod.Mesh
|
||||
)
|
||||
.Resample(300, DensityUnit.PixelsPerInch);
|
||||
|
||||
yield return tImage.CloneImage();
|
||||
}
|
||||
|
||||
if (Configuration.EnableThresholding)
|
||||
{
|
||||
tImage = tImage
|
||||
.NormalizeImage()
|
||||
.RemoveAlpha(MagickColors.White)
|
||||
.ToGrayscale()
|
||||
.ThresholdAdaptive(Configuration.ThresholdWidth, Configuration.ThresholdHeight)
|
||||
.ToBinary();
|
||||
}
|
||||
|
||||
if (Configuration.Border > 0)
|
||||
{
|
||||
tImage = tImage.AddBorder(Configuration.Border, MagickColors.White);
|
||||
}
|
||||
|
||||
yield return tImage;
|
||||
yield return tImage.CloneImage().NegateColors();
|
||||
|
||||
// todo filter large connected components
|
||||
|
||||
// var resized = fluent .CloneObject() .Resize( 0.25f, FilterType.Point,
|
||||
// PixelInterpolateMethod.Integer ); yield return resized.ToImage();
|
||||
//
|
||||
// var hlines = resized .GetHLines() .Select(c => new MagickGeometry(c.Item1, c.Item2, c.Item1 +
|
||||
// 3, c.Item2 + 3)) .ToArray(); Log.Information($"Matched {hlines.Length} geometries");
|
||||
//
|
||||
// var rgbImage = resized.CloneObject().ToRgb(); rgbImage.Fill(MagickColors.Red, hlines); yield
|
||||
// return rgbImage.ToImage();
|
||||
|
||||
// if (Settings.FilterConnectedComponents) { var colorImage = fluent .CloneObject()
|
||||
// .GetConnectedComponents( _connectedComponentsSettings, out var components ) .ToRgb();
|
||||
//
|
||||
// var pixels = colorImage.GetPixels(); colorImage.Fill( MagickColors.Red, components .Where(c
|
||||
// => c.IsFilled(pixels)) .Select(c => c.ToGeometry()) );
|
||||
|
||||
// yield return colorImage.ToImage();
|
||||
}
|
||||
|
||||
#region Overrides of Processor<MagickImage,IMagickImageValueProcessorSettings>
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<MagickImage> Process(IEnumerable<MagickImage> inputs)
|
||||
{
|
||||
return inputs.SelectMany(Process);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Ocr.Tesseract.Models;
|
||||
using Process.Abstract;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract;
|
||||
|
||||
/// <summary>
|
||||
/// Filters <see cref="Word"/>s by confidence
|
||||
/// </summary>
|
||||
public class ConfidenceFilter : Processor<ScanResult, ScanResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum confidence
|
||||
/// </summary>
|
||||
public int Min { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum confidence
|
||||
/// </summary>
|
||||
public int Max { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConfidenceFilter(int min, int max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConfidenceFilter(int min)
|
||||
: this(min, 100)
|
||||
{
|
||||
}
|
||||
|
||||
#region Overrides of Processor<Word,Word>
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScanResult> Process(IEnumerable<ScanResult> inputs)
|
||||
{
|
||||
return inputs.Where(s => s.Word.Confidence >= Min && s.Word.Confidence <= Max);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ocr.Tesseract.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TesseractProcessor"/> configuration
|
||||
/// </summary>
|
||||
public interface ITesseractConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Tesseract data directory path
|
||||
/// </summary>
|
||||
public string DataPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported languages
|
||||
/// </summary>
|
||||
public string[] Languages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tesseract environment variables
|
||||
/// </summary>
|
||||
public IDictionary<string, object> Variables { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Ocr.Tesseract.Models;
|
||||
using Process.Abstract;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract;
|
||||
|
||||
/// <summary>
|
||||
/// Filters duplicate <see cref="Word"/>s
|
||||
/// </summary>
|
||||
public class DuplicateFilter
|
||||
: Processor<ScanResult, ScanResult>
|
||||
{
|
||||
#region Overrides of Processor<KeyValuePair<Word,MagickImage>,KeyValuePair<Word,MagickImage>>
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScanResult> Process(
|
||||
IEnumerable<ScanResult> inputs
|
||||
)
|
||||
{
|
||||
return inputs
|
||||
.GroupBy(sr => sr.Word.Text)
|
||||
.Select(DuplicateSelector)
|
||||
.OrderByDescending(w => w.Word.Confidence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the instance to keep, if duplicates are detected in the input data
|
||||
/// </summary>
|
||||
/// <param name="g">
|
||||
/// <see cref="IGrouping{TKey,TElement}"/>
|
||||
/// containing the duplicate instances
|
||||
/// </param>
|
||||
/// <returns>One single instance to add to the output data</returns>
|
||||
protected virtual ScanResult DuplicateSelector(IGrouping<string, ScanResult> g)
|
||||
{
|
||||
// Default: Return instance with the highest confidence
|
||||
return g.MaxBy(sr => sr.Word.Confidence)!;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using ImageMagick;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IConnectedComponent{TQuantumType}"/> type
|
||||
/// </summary>
|
||||
public static class ConnectedComponentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determine whether this <see cref="IConnectedComponent{TQuantumType}"/> is filled out
|
||||
/// </summary>
|
||||
public static bool IsFilled(
|
||||
this IConnectedComponent<ushort> component,
|
||||
IPixelCollection<ushort> pixels
|
||||
)
|
||||
{
|
||||
var initial = pixels[component.X, component.Y]!.GetChannel(0);
|
||||
return pixels
|
||||
.GetArea(component.X, component.Y, component.Width, component.Height)!
|
||||
.All(p => p == initial);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using ImageMagick;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="MagickImage"/> type
|
||||
/// </summary>
|
||||
public static class MagickImageExtensions
|
||||
{
|
||||
public static double GetIntensity(this MagickImage image)
|
||||
{
|
||||
using var pixels = image.GetPixels();
|
||||
double totalIntensity = pixels.Sum(pixel =>
|
||||
(pixel.GetChannel(0) + pixel.GetChannel(1) + pixel.GetChannel(2)) / 3.0);
|
||||
return totalIntensity / pixels.Count();
|
||||
}
|
||||
|
||||
public static MagickImage ThresholdAdaptive(
|
||||
this MagickImage image,
|
||||
int width, int height, Channels channel = Channels.Gray
|
||||
)
|
||||
{
|
||||
image.AdaptiveThreshold(width, height, channel);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage NegateColors(
|
||||
this MagickImage image
|
||||
)
|
||||
{
|
||||
image.Negate();
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage NormalizeImage(this MagickImage image)
|
||||
{
|
||||
image.Normalize();
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage ToBinary(this MagickImage image)
|
||||
{
|
||||
image.Depth = 1;
|
||||
image.SetBitDepth(1);
|
||||
image.ColorType = ColorType.Bilevel;
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage AddBorder(
|
||||
this MagickImage image,
|
||||
int size, IMagickColor<ushort> color
|
||||
)
|
||||
{
|
||||
image.BorderColor = color;
|
||||
image.Border(size);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage RemoveAlpha(this MagickImage image, IMagickColor<ushort> color)
|
||||
{
|
||||
image.BackgroundColor = color;
|
||||
image.Alpha(AlphaOption.Remove);
|
||||
image.Alpha(AlphaOption.Off);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage Resample(this MagickImage image, double density, DensityUnit unit)
|
||||
{
|
||||
image.Density = new Density(density, unit);
|
||||
image.Resample(new PointD(density));
|
||||
return image;
|
||||
}
|
||||
|
||||
private static int[] hLineKernel =
|
||||
{
|
||||
2, 2, 2,
|
||||
-1, -1, -1,
|
||||
2, 2, 2,
|
||||
// -1, -1, -1
|
||||
};
|
||||
|
||||
private static int[] vLineKernel =
|
||||
{
|
||||
-1, 2, -1,
|
||||
-1, 2, -1,
|
||||
-1, 2, -1
|
||||
};
|
||||
|
||||
public static MagickImage GetHLines(this MagickImage image, out IEnumerable<(int, int)> coords)
|
||||
{
|
||||
var pixels = image.GetPixels();
|
||||
coords = Filter(image, hLineKernel, pixels);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage GetVLines(this MagickImage image, out IEnumerable<(int, int)> coords)
|
||||
{
|
||||
var pixels = image.GetPixels();
|
||||
coords = Filter(image, vLineKernel, pixels);
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the provided <paramref name="kernel"/> and returns a list of coordinates where the filter matches
|
||||
/// </summary>
|
||||
/// <param name="kernel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
private static IEnumerable<(int, int)> Filter(
|
||||
IMagickImage image, int[] kernel, IPixelCollection<ushort> pixels
|
||||
)
|
||||
{
|
||||
const int kernelSize = 3;
|
||||
|
||||
if (kernel.Length != (kernelSize * kernelSize))
|
||||
{
|
||||
throw new ArgumentException($"Filter kernels must be a {kernelSize}x{kernelSize} matrix",
|
||||
nameof(kernel));
|
||||
}
|
||||
|
||||
(int, int)? current = null;
|
||||
for (int x = 0; x < image.Width; x += kernelSize)
|
||||
{
|
||||
for (int y = 0; y < image.Height; y += kernelSize)
|
||||
{
|
||||
if (IsMatch(x, y))
|
||||
{
|
||||
current ??= (x, y);
|
||||
}
|
||||
else if (current.HasValue)
|
||||
{
|
||||
yield return current.Value;
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsMatch(int x, int y)
|
||||
{
|
||||
for (int kernelX = 0; kernelX < 3; kernelX++)
|
||||
{
|
||||
for (int kernelY = 0; kernelY < 3; kernelY++)
|
||||
{
|
||||
var k = kernel[kernelX + (kernelSize * kernelY)];
|
||||
var v = pixels![x + kernelX, y + kernelY]!.GetChannel((int)PixelChannel.Gray);
|
||||
|
||||
switch (k)
|
||||
{
|
||||
case > 0 when v <= 0:
|
||||
case < 0 when v > 0:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static MagickImage ResizeImage(
|
||||
this MagickImage image,
|
||||
float factor,
|
||||
FilterType filterType,
|
||||
PixelInterpolateMethod interpolateMethod
|
||||
)
|
||||
{
|
||||
return image.ResizeImage(
|
||||
(int)Math.Round(image.Width * factor),
|
||||
(int)Math.Round(image.Height * factor),
|
||||
filterType,
|
||||
interpolateMethod
|
||||
);
|
||||
}
|
||||
|
||||
public static MagickImage ResizeImage(
|
||||
this MagickImage image,
|
||||
int width,
|
||||
int height,
|
||||
FilterType filterType,
|
||||
PixelInterpolateMethod interpolateMethod
|
||||
)
|
||||
{
|
||||
image.FilterType = filterType;
|
||||
image.Interpolate = interpolateMethod;
|
||||
image.Resize(new MagickGeometry(width, height));
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage ToRgb(
|
||||
this MagickImage image
|
||||
)
|
||||
{
|
||||
image.ColorSpace = ColorSpace.sRGB;
|
||||
image.ColorType = ColorType.TrueColorAlpha;
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage ToGrayscale(
|
||||
this MagickImage image
|
||||
)
|
||||
{
|
||||
image.Grayscale();
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage Negate(this MagickImage image)
|
||||
{
|
||||
image.Negate();
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage GrayscaleBonW(this MagickImage image)
|
||||
{
|
||||
var intensity = image.GetIntensity();
|
||||
var maxDepth = Math.Pow(2, image.DetermineBitDepth());
|
||||
|
||||
image.Grayscale();
|
||||
if (intensity < maxDepth / 2f)
|
||||
{
|
||||
image.NegateGrayscale();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
public static MagickImage Fill(
|
||||
this MagickImage image,
|
||||
IMagickColor<ushort> color,
|
||||
IEnumerable<IMagickGeometry> geometries
|
||||
)
|
||||
{
|
||||
var drawables = new Drawables().FillColor(color);
|
||||
|
||||
foreach (var component in geometries)
|
||||
{
|
||||
drawables.Rectangle(
|
||||
component.X,
|
||||
component.Y,
|
||||
component.X + component.Width,
|
||||
component.Height + component.Y
|
||||
);
|
||||
}
|
||||
|
||||
drawables.Draw(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static MagickImage GetConnectedComponents(
|
||||
this MagickImage image,
|
||||
IConnectedComponentsSettings settings,
|
||||
out IReadOnlyCollection<IConnectedComponent<ushort>> components
|
||||
)
|
||||
{
|
||||
var tImage = image.CloneImage();
|
||||
components = tImage.ConnectedComponents(settings);
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="MagickImage.Clone()" />
|
||||
public static MagickImage CloneImage(this MagickImage image)
|
||||
{
|
||||
return (MagickImage)image.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Ocr.Tesseract.Models;
|
||||
using System.Collections.Generic;
|
||||
using Tesseract;
|
||||
|
||||
namespace Ocr.Tesseract.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="Page"/> type
|
||||
/// </summary>
|
||||
public static class PageExtensions
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Word"/>s in the given <paramref name="page"/>
|
||||
/// </summary>
|
||||
/// <param name="page">The <see cref="Page"/> to extract words from</param>
|
||||
/// <returns>The extracted <see cref="Word"/>s in the given <paramref name="page"/></returns>
|
||||
public static IEnumerable<Word> GetWords(this Page page)
|
||||
{
|
||||
using var iterator = page.GetIterator();
|
||||
iterator.Begin();
|
||||
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
var word = Word.Parse(iterator);
|
||||
if (string.IsNullOrEmpty(word.Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return word;
|
||||
} while (iterator.Next(PageIteratorLevel.TextLine, PageIteratorLevel.Word));
|
||||
} while (iterator.Next(PageIteratorLevel.Para, PageIteratorLevel.TextLine));
|
||||
} while (iterator.Next(PageIteratorLevel.Block, PageIteratorLevel.Para));
|
||||
} while (iterator.Next(PageIteratorLevel.Block));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ImageMagick;
|
||||
using Ocr.Tesseract.Interface;
|
||||
using Process.Abstract;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract;
|
||||
|
||||
/// <summary>
|
||||
/// Applies various transformation the the input images
|
||||
/// </summary>
|
||||
public abstract class ImageProcessor : Processor<MagickImage, MagickImage>
|
||||
{
|
||||
/// <summary>
|
||||
/// Image processing configuration
|
||||
/// </summary>
|
||||
public IImageProcessorConfiguration Configuration { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected ImageProcessor(IImageProcessorConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Process(IEnumerable{MagickImage})" />
|
||||
public abstract IEnumerable<MagickImage> Process(MagickImage value);
|
||||
|
||||
#region Overrides of Processor<MagickImage,IMagickImageValueProcessorSettings>
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<MagickImage> Process(IEnumerable<MagickImage> inputs)
|
||||
{
|
||||
return inputs.SelectMany(Process);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Ocr.Tesseract.Interface;
|
||||
|
||||
public interface IImageProcessorConfiguration
|
||||
{
|
||||
public int ThresholdWidth { get; set; }
|
||||
public int ThresholdHeight { get; set; }
|
||||
public int Border { get; set; }
|
||||
public bool EnableThresholding { get; set; }
|
||||
public bool EnableResizing { get; set; }
|
||||
public bool FilterConnectedComponents { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using Tesseract;
|
||||
|
||||
namespace Ocr.Tesseract.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Model for <see cref="Word"/> choices
|
||||
/// </summary>
|
||||
public class Choice : IEquatable<Choice>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="confidence"></param>
|
||||
public Choice(string text, float confidence)
|
||||
{
|
||||
Text = text;
|
||||
Confidence = confidence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scanned text
|
||||
/// </summary>
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence that this <see cref="Choice"/> is the correct one
|
||||
/// </summary>
|
||||
public float Confidence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Choice"/> object from a <see cref="ChoiceIterator"/>
|
||||
/// </summary>
|
||||
public static Choice Parse(ChoiceIterator iterator)
|
||||
{
|
||||
return new Choice
|
||||
(
|
||||
iterator.GetText(),
|
||||
iterator.GetConfidence()
|
||||
);
|
||||
}
|
||||
|
||||
#region Overrides of Object
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Equality members
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(Choice? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Text == other.Text;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != this.GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((Choice)obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Text);
|
||||
}
|
||||
|
||||
public static bool operator ==(Choice? left, Choice? right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(Choice? left, Choice? right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ImageMagick;
|
||||
|
||||
namespace Ocr.Tesseract.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Struct for storing <see cref="TesseractProcessor"/> results
|
||||
/// </summary>
|
||||
public struct ScanResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="word"></param>
|
||||
/// <param name="image"></param>
|
||||
public ScanResult(Word word, MagickImage image)
|
||||
{
|
||||
Word = word;
|
||||
Image = image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key referencing <see cref="Image"/>
|
||||
/// </summary>
|
||||
public Word Word { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value referenced by <see cref="Word"/>
|
||||
/// </summary>
|
||||
public MagickImage Image { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Word.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Tesseract;
|
||||
|
||||
namespace Ocr.Tesseract.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Model for storing scanned words
|
||||
/// </summary>
|
||||
public class Word : Choice, IEquatable<Word>
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the <see cref="Word"/> was found in the dictionary
|
||||
/// </summary>
|
||||
public bool IsFromDictionary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the <see cref="Word"/> is numeric
|
||||
/// </summary>
|
||||
public bool Numeric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Other available <see cref="Choice"/>s for this <see cref="Word"/>
|
||||
/// </summary>
|
||||
public ICollection<Choice> Choices { get; set; } = Array.Empty<Choice>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Word(
|
||||
string text,
|
||||
float confidence,
|
||||
bool isFromDictionary,
|
||||
bool isNumeric
|
||||
)
|
||||
: base(text, confidence)
|
||||
{
|
||||
Text = text;
|
||||
Confidence = confidence;
|
||||
IsFromDictionary = isFromDictionary;
|
||||
Numeric = isNumeric;
|
||||
}
|
||||
|
||||
public static Word Parse(ResultIterator iterator)
|
||||
{
|
||||
var word = new Word
|
||||
(
|
||||
iterator.GetText(PageIteratorLevel.Word),
|
||||
iterator.GetConfidence(PageIteratorLevel.Word),
|
||||
iterator.GetWordIsFromDictionary(),
|
||||
iterator.GetWordIsNumeric()
|
||||
);
|
||||
|
||||
if (string.IsNullOrEmpty(word.Text))
|
||||
{
|
||||
// No choices available
|
||||
return word;
|
||||
}
|
||||
|
||||
var choices = new List<Choice>();
|
||||
using var choiceIterator = iterator.GetChoiceIterator();
|
||||
do
|
||||
{
|
||||
choices.Add(Parse(choiceIterator));
|
||||
} while (choiceIterator.Next());
|
||||
|
||||
word.Choices = choices;
|
||||
return word;
|
||||
}
|
||||
|
||||
#region Equality members
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(Word? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.Equals(other)
|
||||
&& IsFromDictionary == other.IsFromDictionary
|
||||
&& Numeric == other.Numeric;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != this.GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((Word)obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(base.GetHashCode(), IsFromDictionary, Numeric);
|
||||
}
|
||||
|
||||
public static bool operator ==(Word? left, Word? right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(Word? left, Word? right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<IncludeSymbols>True</IncludeSymbols>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.2.0" />
|
||||
<PackageReference Include="Magick.NET.Core" Version="13.2.0" />
|
||||
<PackageReference Include="Magick.NET.SystemDrawing" Version="7.0.6" />
|
||||
<PackageReference Include="Tesseract" Version="5.2.0" />
|
||||
<PackageReference Include="Tesseract.Drawing" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Lookup\Lookup.Interface\Lookup.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Process\Process.Abstract\Process.Abstract.csproj" />
|
||||
<ProjectReference Include="..\..\Process\Process.Interface\Process.Interface.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,49 @@
|
||||
using Process.Abstract;
|
||||
using Process.Interface;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ocr.Tesseract;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a <see cref="ProcessingEventDelegate{T}"/>
|
||||
/// once <see cref="IProcessor{TInput,TOutput}.Process(IEnumerable{TInput})"/>
|
||||
/// is called
|
||||
/// </summary>
|
||||
public class ProcessingEvent<T> : Processor<T, T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once this <see cref="IProcessor"/> is executed
|
||||
/// </summary>
|
||||
public event ProcessingEventDelegate<T>? OnProcessing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProcessingEvent()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProcessingEvent(ProcessingEventDelegate<T> onProcessing)
|
||||
{
|
||||
OnProcessing += onProcessing;
|
||||
}
|
||||
|
||||
#region Overrides of Processor<T,T>
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<T> Process(IEnumerable<T> inputs)
|
||||
{
|
||||
var values = inputs.ToArray();
|
||||
OnProcessing?.Invoke(this, values);
|
||||
return values;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event created by <see cref="ProcessingEvent{T}"/>
|
||||
/// </summary>
|
||||
public delegate void ProcessingEventDelegate<T>(
|
||||
IProcessor sender, ICollection<T> inputs
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
using Ocr.Tesseract.Models;
|
||||
using Process.Abstract;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ocr.Tesseract;
|
||||
|
||||
/// <summary>
|
||||
/// Filters words using regular expressions
|
||||
/// </summary>
|
||||
public class RegexFilter : Processor<ScanResult, ScanResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Regex"/> to apply. If the expression
|
||||
/// does not match the input, it will be removed from
|
||||
/// the output data of this processor
|
||||
/// </summary>
|
||||
public Regex Regex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public RegexFilter(Regex regex)
|
||||
{
|
||||
Regex = regex;
|
||||
}
|
||||
|
||||
#region Overrides of Processor<Word,Word>
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScanResult> Process(
|
||||
IEnumerable<ScanResult> inputs
|
||||
)
|
||||
{
|
||||
var matches = inputs
|
||||
.Select(kv => (Kv: kv, Match: Regex.Match(kv.Word.Text)))
|
||||
.Where(m => m.Match.Success)
|
||||
.ToArray();
|
||||
|
||||
foreach (var tuple in matches)
|
||||
{
|
||||
tuple.Kv.Word.Text = tuple.Match.Value;
|
||||
}
|
||||
|
||||
return matches.Select(t => t.Kv);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using ImageMagick;
|
||||
using Ocr.Tesseract.Configuration;
|
||||
using Ocr.Tesseract.Extensions;
|
||||
using Ocr.Tesseract.Models;
|
||||
using Process.Abstract;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Tesseract;
|
||||
|
||||
namespace Ocr.Tesseract
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans <see cref="MagickImage"/>s for <see cref="Word"/>s
|
||||
/// and maps the results to a <see cref="ScanResult"/>
|
||||
/// </summary>
|
||||
public class TesseractProcessor : Processor<MagickImage, ScanResult>
|
||||
{
|
||||
/// <inheritdoc cref="ITesseractConfiguration"/>
|
||||
public ITesseractConfiguration Configuration { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TesseractProcessor(ITesseractConfiguration config)
|
||||
{
|
||||
Configuration = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the provided <paramref name="image"/> for <see cref="Word"/>s
|
||||
/// </summary>
|
||||
/// <param name="image">The <see cref="MagickImage"/> to scan</param>
|
||||
/// <returns>
|
||||
/// A list of <see cref="Word"/>s found
|
||||
/// in the provided <paramref name="image"/>
|
||||
/// </returns>
|
||||
private IEnumerable<Word> Scan(MagickImage image)
|
||||
{
|
||||
// Convert image
|
||||
using var pix = PixConverter.ToPix(image.ToBitmapWithDensity());
|
||||
using var engine = new TesseractEngine(
|
||||
Configuration.DataPath,
|
||||
string.Join('+', Configuration.Languages),
|
||||
EngineMode.Default,
|
||||
Enumerable.Empty<string>(),
|
||||
Configuration.Variables,
|
||||
false
|
||||
)
|
||||
{
|
||||
DefaultPageSegMode = PageSegMode.AutoOsd
|
||||
};
|
||||
|
||||
// Scan
|
||||
return engine
|
||||
.Process(pix)
|
||||
.GetWords()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScanResult> Process(
|
||||
IEnumerable<MagickImage> inputs
|
||||
)
|
||||
{
|
||||
return inputs
|
||||
.SelectMany(Scan, (input, word) => new ScanResult(word, input));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Ocr.Tesseract.Models;
|
||||
using Process.Abstract;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ocr.Tesseract;
|
||||
|
||||
/// <summary>
|
||||
/// Converts the <see cref="Word.Text"/> strings to lowercase
|
||||
/// </summary>
|
||||
public class ToLowerProcessor
|
||||
: Processor<ScanResult, ScanResult>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScanResult> Process(
|
||||
IEnumerable<ScanResult> inputs
|
||||
)
|
||||
{
|
||||
foreach (var kv in inputs)
|
||||
{
|
||||
kv.Word.Text = kv.Word.Text.ToLower();
|
||||
yield return kv;
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.33424.131
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Process", "Process", "{C68DA0CF-A004-4E4F-9A6A-C37E9F38193A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.Interface", "..\Process\Process.Interface\Process.Interface.csproj", "{D3695210-38E3-44E7-A3A5-0FB356B1B57E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.Abstract", "..\Process\Process.Abstract\Process.Abstract.csproj", "{8BC5293F-780B-425E-B90F-9695E917CADA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lookup", "Lookup", "{CFE60455-2706-4A43-B724-B3D6F4CFB003}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Memory", "..\Lookup\Lookup.Memory\Lookup.Memory.csproj", "{3E08171E-04D1-49BC-9DD3-480E2E89B768}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Interface", "..\Lookup\Lookup.Interface\Lookup.Interface.csproj", "{A0CE3D20-DC5C-4449-9562-CDE3339D50E4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.IO", "..\Lookup\Lookup.File\Lookup.IO.csproj", "{BA13C7E2-FAA6-4E79-A92B-803D0E0464BD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Database", "..\Lookup\Lookup.Database\Lookup.Database.csproj", "{BF18A079-9E23-454C-BA9D-B339B854A101}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Abstract", "..\Lookup\Lookup.Abstract\Lookup.Abstract.csproj", "{C16F3302-2E53-4CEF-958E-37EC3308D5EF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract", "Ocr.Tesseract\Ocr.Tesseract.csproj", "{4D62EA05-B3E1-4EA9-A253-56CB76C2BE78}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract.Screenshots", "Ocr.Tesseract.Screenshots\Ocr.Tesseract.Screenshots.csproj", "{46028E7B-3CA3-4E5A-BD28-17D04216068F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D3695210-38E3-44E7-A3A5-0FB356B1B57E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D3695210-38E3-44E7-A3A5-0FB356B1B57E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D3695210-38E3-44E7-A3A5-0FB356B1B57E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D3695210-38E3-44E7-A3A5-0FB356B1B57E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8BC5293F-780B-425E-B90F-9695E917CADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BC5293F-780B-425E-B90F-9695E917CADA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BC5293F-780B-425E-B90F-9695E917CADA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BC5293F-780B-425E-B90F-9695E917CADA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3E08171E-04D1-49BC-9DD3-480E2E89B768}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3E08171E-04D1-49BC-9DD3-480E2E89B768}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3E08171E-04D1-49BC-9DD3-480E2E89B768}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3E08171E-04D1-49BC-9DD3-480E2E89B768}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A0CE3D20-DC5C-4449-9562-CDE3339D50E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A0CE3D20-DC5C-4449-9562-CDE3339D50E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A0CE3D20-DC5C-4449-9562-CDE3339D50E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A0CE3D20-DC5C-4449-9562-CDE3339D50E4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA13C7E2-FAA6-4E79-A92B-803D0E0464BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA13C7E2-FAA6-4E79-A92B-803D0E0464BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA13C7E2-FAA6-4E79-A92B-803D0E0464BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA13C7E2-FAA6-4E79-A92B-803D0E0464BD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF18A079-9E23-454C-BA9D-B339B854A101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF18A079-9E23-454C-BA9D-B339B854A101}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF18A079-9E23-454C-BA9D-B339B854A101}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF18A079-9E23-454C-BA9D-B339B854A101}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C16F3302-2E53-4CEF-958E-37EC3308D5EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C16F3302-2E53-4CEF-958E-37EC3308D5EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C16F3302-2E53-4CEF-958E-37EC3308D5EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C16F3302-2E53-4CEF-958E-37EC3308D5EF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4D62EA05-B3E1-4EA9-A253-56CB76C2BE78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4D62EA05-B3E1-4EA9-A253-56CB76C2BE78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4D62EA05-B3E1-4EA9-A253-56CB76C2BE78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4D62EA05-B3E1-4EA9-A253-56CB76C2BE78}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{46028E7B-3CA3-4E5A-BD28-17D04216068F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{46028E7B-3CA3-4E5A-BD28-17D04216068F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{46028E7B-3CA3-4E5A-BD28-17D04216068F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{46028E7B-3CA3-4E5A-BD28-17D04216068F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{D3695210-38E3-44E7-A3A5-0FB356B1B57E} = {C68DA0CF-A004-4E4F-9A6A-C37E9F38193A}
|
||||
{8BC5293F-780B-425E-B90F-9695E917CADA} = {C68DA0CF-A004-4E4F-9A6A-C37E9F38193A}
|
||||
{3E08171E-04D1-49BC-9DD3-480E2E89B768} = {CFE60455-2706-4A43-B724-B3D6F4CFB003}
|
||||
{A0CE3D20-DC5C-4449-9562-CDE3339D50E4} = {CFE60455-2706-4A43-B724-B3D6F4CFB003}
|
||||
{BA13C7E2-FAA6-4E79-A92B-803D0E0464BD} = {CFE60455-2706-4A43-B724-B3D6F4CFB003}
|
||||
{BF18A079-9E23-454C-BA9D-B339B854A101} = {CFE60455-2706-4A43-B724-B3D6F4CFB003}
|
||||
{C16F3302-2E53-4CEF-958E-37EC3308D5EF} = {CFE60455-2706-4A43-B724-B3D6F4CFB003}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DFA659EE-FE78-4BD9-888B-78984354093E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,3 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Magick/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tesseract/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
Reference in New Issue
Block a user