diff --git a/Directory.Packages.props b/Directory.Packages.props index e21b4bd6..f2af0e7e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,6 +12,7 @@ + diff --git a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/GoogleAIOcr.cs b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/GoogleAIOcr.cs index 09a9065e..20432113 100644 --- a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/GoogleAIOcr.cs +++ b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/GoogleAIOcr.cs @@ -51,7 +51,10 @@ public GoogleAIOcr(IOptionsSnapshot langOptions, IOptionsSnapsh systemInstruction: system); } - public async ValueTask> RecognizeAsync(SoftwareBitmap bitmap) + public ValueTask> RecognizeAsync(OcrCaptureInput input) + => OcrUtility.RecognizeRegionsAsync(input, (bitmap, _) => RecognizeRegionAsync(bitmap)); + + private async ValueTask> RecognizeRegionAsync(SoftwareBitmap bitmap) { var base64 = await bitmap.EncodeToJpegBase64().ConfigureAwait(false); var req = new GenerateContentRequest(); @@ -71,7 +74,8 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm var widthPx = xMaxPx - xMinPx; var heightPx = yMaxPx - yMinPx; return new TextRect(rect.Text, xMinPx, yMinPx, widthPx, heightPx, heightPx, false); - }); + }) + .ToArray(); } private record Rect(int[] Box2d, string Text); diff --git a/Plugins/WindowTranslator.Plugin.LLMPlugin/LLMOcr.cs b/Plugins/WindowTranslator.Plugin.LLMPlugin/LLMOcr.cs index 97ec1b42..2a9ac01f 100644 --- a/Plugins/WindowTranslator.Plugin.LLMPlugin/LLMOcr.cs +++ b/Plugins/WindowTranslator.Plugin.LLMPlugin/LLMOcr.cs @@ -69,7 +69,6 @@ public LLMOcr(IOptionsSnapshot langOptions, IOptionsSnapshot langOptions, IOptionsSnapshot> RecognizeAsync(SoftwareBitmap bitmap) + public ValueTask> RecognizeAsync(OcrCaptureInput input) + => OcrUtility.RecognizeRegionsAsync(input, (bitmap, _) => RecognizeRegionAsync(bitmap)); + + private async ValueTask> RecognizeRegionAsync(SoftwareBitmap bitmap) { var bytes = await bitmap.EncodeToJpegBytes().ConfigureAwait(false); var image = BinaryData.FromBytes(bytes); @@ -147,7 +149,8 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm var widthPx = xMaxPx - xMinPx; var heightPx = yMaxPx - yMinPx; return new TextRect(rect.Text, xMinPx, yMinPx, widthPx, heightPx, heightPx, false); - }); + }) + .ToArray(); } catch (Exception ex) { @@ -158,4 +161,4 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm private record Rect([property: JsonPropertyName("box_2d")] int[] Box2d, string Text); private record RecognizedTexts(Rect[] Texts); -} \ No newline at end of file +} diff --git a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/OneOcr.cs b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/OneOcr.cs index f480690f..5a35a300 100644 --- a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/OneOcr.cs +++ b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/OneOcr.cs @@ -128,37 +128,31 @@ public void Dispose() this.fastText?.Dispose(); } - public async ValueTask> RecognizeAsync(SoftwareBitmap bitmap) - { - // リサイズ処理(scale != 1.0 の場合は新しいビットマップを生成) - var workingBitmap = await bitmap.ResizeSoftwareBitmapAsync(this.scale); + public ValueTask> RecognizeAsync(OcrCaptureInput input) + => OcrUtility.RecognizeRegionsAsync( + input, + RecognizeRegionAsync, + this.scale, + this.brightness, + this.contrast); - // 明るさ・コントラスト調整(インプレース) - // scale == 1.0 の場合はリサイズで元のビットマップが返るため、コピーを作成してから調整 - if (this.brightness != 0 || this.contrast != 0) - { - if (workingBitmap == bitmap) - { - // 元のビットマップを変更しないようにコピーを作成 -#pragma warning disable CA1416 // プラットフォームの互換性を検証 - workingBitmap = SoftwareBitmap.Copy(bitmap); -#pragma warning restore CA1416 // プラットフォームの互換性を検証 - } - workingBitmap.AdjustBrightnessContrastInPlace(this.brightness, this.contrast); - } + /// + /// 指定した画像のテキストを認識する + /// + /// 認識対象の画像 + /// 拡大後の全体画像サイズ + private async ValueTask> RecognizeRegionAsync( + SoftwareBitmap workingBitmap, + System.Drawing.Size sourceSize) + { // テキスト認識処理をバックグラウンドで実行 var textRects = await Task.Run(() => Recognize(workingBitmap)).ConfigureAwait(false); // 認識したテキスト矩形の補正と結合処理を実行 - textRects = ProcessTextRects(textRects, workingBitmap.PixelWidth, workingBitmap.PixelHeight); - - if (bitmap != workingBitmap) - { - workingBitmap.Dispose(); - } + textRects = ProcessTextRects(textRects, sourceSize.Width, sourceSize.Height); - var wFat = bitmap.PixelWidth * 0.004; + var wFat = sourceSize.Width * 0.004; return textRects // マージ後に少なすぎる文字も認識ミス扱い @@ -391,23 +385,13 @@ private TextRect ToTextRect(TextRectMerger mergedRect) { var (x, y, width, height, fontSize, text) = mergedRect; - // スケールに応じた座標変換 - if (this.scale != 1.0) - { - x /= scale; - y /= scale; - width /= scale; - height /= scale; - fontSize /= scale; - } - // 高さがフォントサイズの2倍以上の場合は複数行とみなす var lines = height / fontSize >= 2; // 結合された矩形の平均角度を計算 var angle = mergedRect.Rects.Average(r => r.Angle); - return new(text, x, y, width, height, fontSize, lines) { Angle = angle }; + return new TextRect(text, x, y, width, height, fontSize, lines) { Angle = angle }; } /// diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/TesseractOcr.cs b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/TesseractOcr.cs index 3e81a7bc..e1d2fda0 100644 --- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/TesseractOcr.cs +++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/TesseractOcr.cs @@ -43,30 +43,26 @@ public sealed class TesseractOcr( private readonly int brightness = ocrParam.Value.Brightness; private readonly int contrast = ocrParam.Value.Contrast; - public async ValueTask> RecognizeAsync(SoftwareBitmap bitmap) + public ValueTask> RecognizeAsync(OcrCaptureInput input) + => OcrUtility.RecognizeRegionsAsync( + input, + RecognizeRegionAsync, + this.scale, + this.brightness, + this.contrast, + this.cts.Token); + + /// + /// 指定した画像のテキストを認識する + /// + /// 認識対象の画像 + /// 拡大後の全体画像サイズ + private async ValueTask> RecognizeRegionAsync(SoftwareBitmap bitmap, System.Drawing.Size sourceSize) { - // リサイズ処理(scale != 1.0 の場合は新しいビットマップを生成) - var workingBitmap = await bitmap.ResizeSoftwareBitmapAsync(this.scale, this.cts.Token); - this.cts.Token.ThrowIfCancellationRequested(); - - // 明るさ・コントラスト調整(インプレース) - // scale == 1.0 の場合はリサイズで元のビットマップが返るため、コピーを作成してから調整 - if (this.brightness != 0 || this.contrast != 0) - { - if (workingBitmap == bitmap) - { - // 元のビットマップを変更しないようにコピーを作成 -#pragma warning disable CA1416 // プラットフォームの互換性を検証 - workingBitmap = SoftwareBitmap.Copy(bitmap); -#pragma warning restore CA1416 // プラットフォームの互換性を検証 - } - workingBitmap.AdjustBrightnessContrastInPlace(this.brightness, this.contrast); - } - this.cts.Token.ThrowIfCancellationRequested(); var sw = Stopwatch.StartNew(); // テキスト認識処理をバックグラウンドで実行 - var textRects = await Task.Run(async () => await Recognize(workingBitmap).ConfigureAwait(false), this.cts.Token).ConfigureAwait(false); + var textRects = await Task.Run(async () => await Recognize(bitmap).ConfigureAwait(false), this.cts.Token).ConfigureAwait(false); this.cts.Token.ThrowIfCancellationRequested(); this.logger.LogDebug($"Recognize: {sw.Elapsed}"); @@ -76,8 +72,9 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm } // マージ処理 - var xt = xPosThreshold * bitmap.PixelWidth; - var yt = yPosThreshold * bitmap.PixelHeight; + // 認識結果はスケール後画像の座標系なので、マージ閾値も同じ座標系に揃える + var xt = xPosThreshold * sourceSize.Width; + var yt = yPosThreshold * sourceSize.Height; var results = new List(textRects.Length); var queue = new RemovableQueue(textRects.OrderBy(r => r.Y)); @@ -114,18 +111,14 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm results.Add(temp); } - if (bitmap != workingBitmap) - { - workingBitmap.Dispose(); - } - return results - .Select(r => ToTextRect(r, this.scale)) + .Select(ToTextRect) // マージ後に少なすぎる文字も認識ミス扱い // 特殊なグリフの言語は対象外(日本語、中国語、韓国語、ロシア語) .Where(w => IsSpecialLang(this.source) || w.SourceText.Length > 2) // 全部数字・記号なら対象外 - .Where(w => !AllSymbolOrSpace().IsMatch(w.SourceText)); + .Where(w => !AllSymbolOrSpace().IsMatch(w.SourceText)) + .ToArray(); } private async ValueTask Recognize(SoftwareBitmap bitmap) @@ -298,17 +291,10 @@ public TextRect ToRect() } } - private static TextRect ToTextRect(TempMergeRect combinedRect, double scale) + private static TextRect ToTextRect(TempMergeRect combinedRect) { var (x, y, width, height, fontSize, _) = combinedRect; var text = combinedRect.Text; - // 元の画像座標に変換 - x /= scale; - y /= scale; - width /= scale; - height /= scale; - fontSize /= scale; - // 高さがフォントサイズの2倍以上の場合は複数行とみなす var lines = height / fontSize >= 2; @@ -319,7 +305,7 @@ private static TextRect ToTextRect(TempMergeRect combinedRect, double scale) height += fontSize * fat; y -= fontSize * fat * .5; - return new(text, x, y, width, height, fontSize, lines); + return new TextRect(text, x, y, width, height, fontSize, lines); } public void Dispose() diff --git a/Sandbox/Program.cs b/Sandbox/Program.cs index 875b3d75..e3c80313 100644 --- a/Sandbox/Program.cs +++ b/Sandbox/Program.cs @@ -126,7 +126,7 @@ static async Task ClipTextRect([Argument] string imagePath, [FromServices] ILogg var bitmap = await decoder.GetSoftwareBitmapAsync(); // OCRの実行 - var textRects = await ocr.RecognizeAsync(bitmap); + var textRects = await ocr.RecognizeAsync(new(bitmap, [])); // 画像からテキスト矩形を切り抜き var outputDir = Path.Combine(Path.GetDirectoryName(imagePath)!, "clipped"); diff --git a/WindowTranslator.Abstractions/BitmapUtility.cs b/WindowTranslator.Abstractions/BitmapUtility.cs index 68f1d76d..b06740f2 100644 --- a/WindowTranslator.Abstractions/BitmapUtility.cs +++ b/WindowTranslator.Abstractions/BitmapUtility.cs @@ -224,6 +224,43 @@ public static async ValueTask TrySaveImage(this SoftwareBitmap source, string pa // ここで何かログを残すことも可能ですが、今回は省略します } } + + /// + /// 画像を切り出す + /// + /// 元の画像 + /// 切り出し開始位置のX座標 + /// 切り出し開始位置のY座標 + /// 切り出す幅 + /// 切り出す高さ + /// 切り出された画像 + internal static unsafe SoftwareBitmap Crop(this SoftwareBitmap source, int x, int y, int width, int height) + { + var cropped = new SoftwareBitmap(source.BitmapPixelFormat, width, height, source.BitmapAlphaMode); + + using var sourceBuffer = source.LockBuffer(BitmapBufferAccessMode.Read); + using var croppedBuffer = cropped.LockBuffer(BitmapBufferAccessMode.Write); + using var sourceReference = sourceBuffer.CreateReference(); + using var croppedReference = croppedBuffer.CreateReference(); + + sourceReference.As().GetBuffer(out var sourceData, out _); + croppedReference.As().GetBuffer(out var croppedData, out _); + + var bytesPerPixel = 4; // BGRA8 + var sourceStride = sourceBuffer.GetPlaneDescription(0).Stride; + var croppedStride = croppedBuffer.GetPlaneDescription(0).Stride; + + for (int row = 0; row < height; row++) + { + var sourceOffset = ((y + row) * sourceStride) + (x * bytesPerPixel); + var croppedOffset = row * croppedStride; + + new ReadOnlySpan(sourceData + sourceOffset, width * bytesPerPixel) + .CopyTo(new Span(croppedData + croppedOffset, width * bytesPerPixel)); + } + + return cropped; + } } [ComImport] diff --git a/WindowTranslator.Abstractions/Modules/IOcrModule.cs b/WindowTranslator.Abstractions/Modules/IOcrModule.cs index 13b68cad..35e205df 100644 --- a/WindowTranslator.Abstractions/Modules/IOcrModule.cs +++ b/WindowTranslator.Abstractions/Modules/IOcrModule.cs @@ -15,12 +15,30 @@ public interface IOcrModule #if WINDOWS /// - /// 画像からテキストを認識する + /// 1回のキャプチャーに含まれるOCR対象画像からテキストを認識する /// - ValueTask> RecognizeAsync(Windows.Graphics.Imaging.SoftwareBitmap bitmap); + ValueTask> RecognizeAsync(OcrCaptureInput input); #endif } +#if WINDOWS +/// +/// 1回のキャプチャーに対するOCR入力 +/// +/// 全体のコンテキストを把握するための元画像 +/// 実際にOCRする範囲の一覧 +public sealed record OcrCaptureInput( + Windows.Graphics.Imaging.SoftwareBitmap Source, + IReadOnlyList Regions); + +/// +/// 1つのOCR対象範囲 +/// +/// 全体画像上の切り出し範囲 +/// 翻訳コンテキストに設定するキーワード。未指定の場合は認識結果のコンテキストを維持する +public sealed record OcrRegionInput(RectInfo Bounds, string? Keyword = null); +#endif + /// /// 基本的なOCRパラメータ /// @@ -94,4 +112,13 @@ public class BasicOcrParam : IPluginParam [Category("MergeThrethold")] public bool IsAvoidMergeList { get; set; } = false; + /// + /// OCR対象範囲のリスト + /// + /// + /// 1件以上設定されている場合は、画像全体ではなく指定範囲内だけをOCRする。 + /// 範囲が重なる場合は、リストの順序が優先度を表す(前方が高優先度)。 + /// + [Category("PriorityRect")] + public List PriorityRects { get; set; } = []; } diff --git a/WindowTranslator.Abstractions/OcrUtility.cs b/WindowTranslator.Abstractions/OcrUtility.cs index a48a417e..94e4dd23 100644 --- a/WindowTranslator.Abstractions/OcrUtility.cs +++ b/WindowTranslator.Abstractions/OcrUtility.cs @@ -7,7 +7,102 @@ namespace WindowTranslator; /// public static partial class OcrUtility { +#if WINDOWS + private const double OverlapThreshold = 0.5; + + /// + /// 1回のキャプチャーに含まれるOCR対象範囲を順番に切り出して認識する + /// + /// 全体画像とOCR対象範囲 + /// 切り出した画像と拡大後の全体画像サイズを認識する処理 + /// OCR前に画像へ適用する拡大率 + /// OCR前に適用する明るさ + /// OCR前に適用するコントラスト + /// キャンセルトークン + /// 全体画像の座標系へ変換し、優先度の重複を除外した認識結果 + public static async ValueTask> RecognizeRegionsAsync( + Modules.OcrCaptureInput input, + Func>> recognizeAsync, + double scale = 1, + int brightness = 0, + int contrast = 0, + CancellationToken cancellationToken = default) + { + var results = new List(); + var scaledSourceSize = new System.Drawing.Size( + (int)(input.Source.PixelWidth * scale), + (int)(input.Source.PixelHeight * scale)); + + foreach (var region in input.Regions) + { + var bounds = region.Bounds; + var isSource = bounds.X == 0 + && bounds.Y == 0 + && bounds.Width == input.Source.PixelWidth + && bounds.Height == input.Source.PixelHeight; + var bitmap = isSource + ? input.Source + : input.Source.Crop((int)bounds.X, (int)bounds.Y, (int)bounds.Width, (int)bounds.Height); + var workingBitmap = bitmap; + + try + { + workingBitmap = await bitmap.ResizeSoftwareBitmapAsync(scale, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (brightness != 0 || contrast != 0) + { + if (workingBitmap == bitmap) + { + workingBitmap = Windows.Graphics.Imaging.SoftwareBitmap.Copy(bitmap); + } + workingBitmap.AdjustBrightnessContrastInPlace(brightness, contrast); + } + + var regionResults = await recognizeAsync(workingBitmap, scaledSourceSize).ConfigureAwait(false); + var offsetResults = regionResults + .Select(r => r with + { + X = r.X / scale, + Y = r.Y / scale, + Width = r.Width / scale, + Height = r.Height / scale, + FontSize = r.FontSize / scale, + }) + .Select(r => r.Offset(bounds.X, bounds.Y, region.Keyword)) + .Where(r => !IsCoveredBy(r, results)) + .ToArray(); + results.AddRange(offsetResults); + } + finally + { + if (workingBitmap != bitmap) + { + workingBitmap.Dispose(); + } + if (!isSource) + { + bitmap.Dispose(); + } + } + } + + return results; + } + + private static bool IsCoveredBy(TextRect text, List recognized) + { + var box = text.GetRotatedBoundingBox(); + return recognized.Any(r => IntersectionRatio(r.GetRotatedBoundingBox(), box) >= OverlapThreshold); + } + + private static double IntersectionRatio(RectInfo area, RectInfo other) + { + var width = Math.Max(0, Math.Min(area.Right, other.Right) - Math.Max(area.Left, other.Left)); + var height = Math.Max(0, Math.Min(area.Bottom, other.Bottom) - Math.Max(area.Top, other.Top)); + return width * height / (other.Width * other.Height); + } +#endif [GeneratedRegex(@"^[\s\p{S}\p{P}\d]+$")] public static partial Regex AllSymbolOrSpace(); -} \ No newline at end of file +} diff --git a/WindowTranslator.Abstractions/PriorityRect.cs b/WindowTranslator.Abstractions/PriorityRect.cs new file mode 100644 index 00000000..5acad27e --- /dev/null +++ b/WindowTranslator.Abstractions/PriorityRect.cs @@ -0,0 +1,35 @@ +namespace WindowTranslator; + +/// +/// OCR対象範囲を表す矩形情報 +/// +/// X位置(左上角のX座標、画像幅に対する相対値 0.0-1.0) +/// Y位置(左上角のY座標、画像高さに対する相対値 0.0-1.0) +/// 幅(画像幅に対する相対値 0.0-1.0) +/// 高さ(画像高さに対する相対値 0.0-1.0) +/// キーワード(翻訳コンテキストに使用) +public record PriorityRect(double X, double Y, double Width, double Height, string Keyword = "") +{ + /// + /// 絶対座標に変換する + /// + /// 画像の幅 + /// 画像の高さ + /// 絶対座標の矩形情報 + public RectInfo ToAbsoluteRect(int imageWidth, int imageHeight) + => new(X * imageWidth, Y * imageHeight, Width * imageWidth, Height * imageHeight); + + /// + /// 絶対座標から相対座標のOCR対象範囲を作成する + /// + /// X位置(絶対座標) + /// Y位置(絶対座標) + /// 幅(絶対座標) + /// 高さ(絶対座標) + /// 画像の幅 + /// 画像の高さ + /// キーワード + /// 相対座標のOCR対象範囲 + public static PriorityRect FromAbsoluteRect(double x, double y, double width, double height, double imageWidth, double imageHeight, string keyword = "") + => new(x / imageWidth, y / imageHeight, width / imageWidth, height / imageHeight, keyword); +} diff --git a/WindowTranslator.Abstractions/Properties/Resources.ar.resx b/WindowTranslator.Abstractions/Properties/Resources.ar.resx index 3208883f..da0cfec6 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.ar.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.ar.resx @@ -110,4 +110,10 @@ أخرى - \ No newline at end of file + + منطقة OCR + + + مناطق OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.cs.resx b/WindowTranslator.Abstractions/Properties/Resources.cs.resx index 1f02740c..15c3b921 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.cs.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.cs.resx @@ -180,4 +180,10 @@ Modul mezipaměti + + Oblast OCR + + + Oblasti OCR + diff --git a/WindowTranslator.Abstractions/Properties/Resources.de.resx b/WindowTranslator.Abstractions/Properties/Resources.de.resx index 557324f0..f5773d15 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.de.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.de.resx @@ -168,4 +168,10 @@ Sonstiges - \ No newline at end of file + + OCR-Bereich + + + OCR-Bereiche + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.en.resx b/WindowTranslator.Abstractions/Properties/Resources.en.resx index 52cdbffb..2bb08041 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.en.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.en.resx @@ -168,4 +168,10 @@ Other - \ No newline at end of file + + OCR Region + + + OCR Regions + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.es.resx b/WindowTranslator.Abstractions/Properties/Resources.es.resx index 0447bb74..4a51d9f4 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.es.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.es.resx @@ -110,4 +110,10 @@ Otros - \ No newline at end of file + + Área de OCR + + + Áreas de OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.fa.resx b/WindowTranslator.Abstractions/Properties/Resources.fa.resx index 56ebb681..568c97e1 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.fa.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.fa.resx @@ -110,4 +110,10 @@ متفرقه + + ناحیه OCR + + + ناحیه‌های OCR + diff --git a/WindowTranslator.Abstractions/Properties/Resources.fil.resx b/WindowTranslator.Abstractions/Properties/Resources.fil.resx index 1e955451..3bbc39cf 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.fil.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.fil.resx @@ -122,4 +122,10 @@ Modyul ng Cache - \ No newline at end of file + + Saklaw ng OCR + + + Mga Saklaw ng OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.fr.resx b/WindowTranslator.Abstractions/Properties/Resources.fr.resx index 7eb50e2d..8ed7d3dc 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.fr.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.fr.resx @@ -110,4 +110,10 @@ Autres - \ No newline at end of file + + Zone d’OCR + + + Zones d’OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.hi.resx b/WindowTranslator.Abstractions/Properties/Resources.hi.resx index 46f3a27c..3efebb6d 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.hi.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.hi.resx @@ -110,4 +110,10 @@ अन्य - \ No newline at end of file + + OCR क्षेत्र + + + OCR क्षेत्र + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.hu.resx b/WindowTranslator.Abstractions/Properties/Resources.hu.resx index 768144d8..a26ea585 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.hu.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.hu.resx @@ -67,4 +67,10 @@ Gyorsítótár modul + + OCR-terület + + + OCR-területek + diff --git a/WindowTranslator.Abstractions/Properties/Resources.id.resx b/WindowTranslator.Abstractions/Properties/Resources.id.resx index 5d58d378..c8e9fad4 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.id.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.id.resx @@ -110,4 +110,10 @@ Lainnya - \ No newline at end of file + + Area OCR + + + Area OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.ko.resx b/WindowTranslator.Abstractions/Properties/Resources.ko.resx index 37a8e32a..d9d9ae99 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.ko.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.ko.resx @@ -168,4 +168,10 @@ 기타 - \ No newline at end of file + + OCR 영역 + + + OCR 영역 + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.ms.resx b/WindowTranslator.Abstractions/Properties/Resources.ms.resx index 1c414402..85364368 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.ms.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.ms.resx @@ -110,4 +110,10 @@ Lain-lain - \ No newline at end of file + + Kawasan OCR + + + Kawasan OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.pl.resx b/WindowTranslator.Abstractions/Properties/Resources.pl.resx index 4f528c4b..33457620 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.pl.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.pl.resx @@ -180,4 +180,10 @@ Moduł pamięci podręcznej - \ No newline at end of file + + Obszar OCR + + + Obszary OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.pt-BR.resx b/WindowTranslator.Abstractions/Properties/Resources.pt-BR.resx index 4ae33922..0c1b6271 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.pt-BR.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.pt-BR.resx @@ -110,4 +110,10 @@ Outros + + Área de OCR + + + Áreas de OCR + diff --git a/WindowTranslator.Abstractions/Properties/Resources.resx b/WindowTranslator.Abstractions/Properties/Resources.resx index 14ed80dc..4c864ac3 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.resx @@ -180,4 +180,10 @@ キャッシュモジュール - \ No newline at end of file + + OCR範囲 + + + OCR対象範囲 + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.ru.resx b/WindowTranslator.Abstractions/Properties/Resources.ru.resx index 10836a2c..825df5a0 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.ru.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.ru.resx @@ -122,4 +122,10 @@ Модуль кэша - \ No newline at end of file + + Область OCR + + + Области OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.th.resx b/WindowTranslator.Abstractions/Properties/Resources.th.resx index 6fd287c5..fff6e75b 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.th.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.th.resx @@ -122,4 +122,10 @@ โมดูลแคช - \ No newline at end of file + + พื้นที่ OCR + + + พื้นที่ OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.tr.resx b/WindowTranslator.Abstractions/Properties/Resources.tr.resx index f18ab918..0f1f2e63 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.tr.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.tr.resx @@ -122,4 +122,10 @@ Önbellek Modülü - \ No newline at end of file + + OCR Alanı + + + OCR Alanları + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.vi.resx b/WindowTranslator.Abstractions/Properties/Resources.vi.resx index a05765bf..fabbd67e 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.vi.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.vi.resx @@ -168,4 +168,10 @@ người khác - \ No newline at end of file + + Vùng OCR + + + Các vùng OCR + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.zh-CN.resx b/WindowTranslator.Abstractions/Properties/Resources.zh-CN.resx index 93a29a8d..7ed50c61 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.zh-CN.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.zh-CN.resx @@ -168,4 +168,10 @@ 其他 - \ No newline at end of file + + OCR 区域 + + + OCR 区域 + + diff --git a/WindowTranslator.Abstractions/Properties/Resources.zh-TW.resx b/WindowTranslator.Abstractions/Properties/Resources.zh-TW.resx index 7f54a9bb..d652613d 100644 --- a/WindowTranslator.Abstractions/Properties/Resources.zh-TW.resx +++ b/WindowTranslator.Abstractions/Properties/Resources.zh-TW.resx @@ -168,4 +168,10 @@ 其他 - \ No newline at end of file + + OCR 區域 + + + OCR 區域 + + diff --git a/WindowTranslator.Abstractions/TextRect.cs b/WindowTranslator.Abstractions/TextRect.cs index f38b1733..84ab3c88 100644 --- a/WindowTranslator.Abstractions/TextRect.cs +++ b/WindowTranslator.Abstractions/TextRect.cs @@ -150,6 +150,7 @@ public readonly record struct RectInfo(double X, double Y, double Width, double /// 重なっている場合はtrue、そうでなければfalse public bool OverlapsWith(RectInfo other) => !(Right <= other.Left || other.Right <= Left || Bottom <= other.Top || other.Bottom <= Top); + } /// @@ -163,4 +164,27 @@ public record TextInfo(string SourceText, string? TranslatedText) /// このテキストの文脈 /// public string Context { get; init; } = string.Empty; -}; \ No newline at end of file +}; + +/// +/// TextRectの拡張メソッド +/// +public static class TextRectExtensions +{ + /// + /// TextRectの座標をオフセット分移動する + /// + /// 元のTextRect + /// X方向のオフセット + /// Y方向のオフセット + /// キーワード(コンテキスト)。未指定の場合は元のコンテキストを維持する + /// オフセットされたTextRect + internal static TextRect Offset(this TextRect rect, double offsetX, double offsetY, string? keyword = null) + => rect with + { + X = rect.X + offsetX, + Y = rect.Y + offsetY, + Context = keyword ?? rect.Context + }; + +} diff --git a/WindowTranslator.Tests/OcrUtilityTests.cs b/WindowTranslator.Tests/OcrUtilityTests.cs new file mode 100644 index 00000000..d7eb03d0 --- /dev/null +++ b/WindowTranslator.Tests/OcrUtilityTests.cs @@ -0,0 +1,198 @@ +using Windows.Graphics.Imaging; +using WindowTranslator.Modules; + +namespace WindowTranslator.Tests; + +/// +/// OCR対象範囲の認識処理に関するテスト +/// +public class OcrUtilityTests +{ + private const int Width = 400; + private const int Height = 300; + + private static SoftwareBitmap CreateBitmap() + => new(BitmapPixelFormat.Bgra8, Width, Height, BitmapAlphaMode.Premultiplied); + + private static TextRect Text(string text, double x, double y, double width = 40, double height = 20) + => new(text, x, y, width, height, height, false); + + [Fact] + public async Task 全体範囲は元画像をそのまま認識してコンテキストを維持する() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [new(new(0, 0, Width, Height))]); + + var results = await OcrUtility.RecognizeRegionsAsync(input, (target, _) => + { + Assert.Same(bitmap, target); + return ValueTask.FromResult>([ + Text("full", 10, 10) with { Context = "module context" }, + ]); + }); + + Assert.Equal("module context", Assert.Single(results).Context); + } + + [Fact] + public async Task 指定範囲を切り出して全体画像座標とコンテキストへ変換する() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [new(new(101, 150, 101, 77), "context")]); + + var results = await OcrUtility.RecognizeRegionsAsync(input, (target, _) => + { + Assert.NotSame(bitmap, target); + Assert.Equal(101, target.PixelWidth); + Assert.Equal(77, target.PixelHeight); + return ValueTask.FromResult>([Text("region", 10, 20)]); + }); + + var result = Assert.Single(results); + Assert.Equal(111, result.X); + Assert.Equal(170, result.Y); + Assert.Equal("context", result.Context); + } + + [Fact] + public async Task 拡大画像の認識結果を元画像座標へ戻す() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [new(new(100, 150, 100, 75), "context")]); + + var results = await OcrUtility.RecognizeRegionsAsync( + input, + (target, sourceSize) => + { + Assert.Equal(200, target.PixelWidth); + Assert.Equal(150, target.PixelHeight); + Assert.Equal(new System.Drawing.Size(800, 600), sourceSize); + return ValueTask.FromResult>([ + Text("scaled", 20, 40, 80, 40) with { Angle = 30 }, + ]); + }, + scale: 2); + + var result = Assert.Single(results); + Assert.Equal(110, result.X); + Assert.Equal(170, result.Y); + Assert.Equal(40, result.Width); + Assert.Equal(20, result.Height); + Assert.Equal(20, result.FontSize); + Assert.Equal(30, result.Angle); + Assert.Equal("context", result.Context); + } + + [Fact] + public async Task 画像補正時は元画像を変更しない() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [new(new(0, 0, Width, Height))]); + + await OcrUtility.RecognizeRegionsAsync( + input, + (target, _) => + { + Assert.NotSame(bitmap, target); + return ValueTask.FromResult>([]); + }, + brightness: 1); + } + + [Fact] + public async Task 対象範囲が空の場合は認識しない() + { + using var bitmap = CreateBitmap(); + var calls = 0; + + var results = await OcrUtility.RecognizeRegionsAsync( + new(bitmap, []), + (_, _) => + { + calls++; + return ValueTask.FromResult>([]); + }); + + Assert.Equal(0, calls); + Assert.Empty(results); + } + + [Fact] + public async Task 高優先度の結果と50パーセント重なる低優先度の結果を破棄する() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [ + new(new(0, 0, 200, 150), "high"), + new(new(0, 0, 200, 150), "low"), + ]); + var regionResults = new Queue>([ + [Text("high text", 10, 10)], + [Text("low text", 30, 10)], + ]); + + var results = await OcrUtility.RecognizeRegionsAsync( + input, + (_, _) => ValueTask.FromResult(regionResults.Dequeue())); + + var result = Assert.Single(results); + Assert.Equal("high text", result.SourceText); + Assert.Equal("high", result.Context); + } + + [Fact] + public async Task 同じ対象範囲内で重なる認識結果は両方を保持する() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [new(new(0, 0, 200, 150), "context")]); + + var results = await OcrUtility.RecognizeRegionsAsync( + input, + (_, _) => ValueTask.FromResult>([ + Text("first", 10, 10), + Text("second", 20, 10), + ])); + + Assert.Equal(["first", "second"], results.Select(r => r.SourceText)); + } + + [Fact] + public async Task 高優先度の結果との重なりが50パーセント未満なら両方を保持する() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [ + new(new(0, 0, 200, 150), "high"), + new(new(0, 0, 200, 150), "low"), + ]); + var regionResults = new Queue>([ + [Text("high text", 10, 10)], + [Text("low text", 40, 10)], + ]); + + var results = await OcrUtility.RecognizeRegionsAsync( + input, + (_, _) => ValueTask.FromResult(regionResults.Dequeue())); + + Assert.Equal(["high text", "low text"], results.Select(r => r.SourceText)); + Assert.Equal(["high", "low"], results.Select(r => r.Context)); + } + + [Fact] + public async Task 対象範囲が重なっても認識結果が重ならなければ両方を保持する() + { + using var bitmap = CreateBitmap(); + var input = new OcrCaptureInput(bitmap, [ + new(new(0, 0, 200, 150), "high"), + new(new(100, 0, 200, 150), "low"), + ]); + var regionResults = new Queue>([ + [Text("high text", 10, 10)], + [Text("low text", 80, 10)], + ]); + + var results = await OcrUtility.RecognizeRegionsAsync( + input, + (_, _) => ValueTask.FromResult(regionResults.Dequeue())); + + Assert.Equal(["high text", "low text"], results.Select(r => r.SourceText)); + } +} diff --git a/WindowTranslator.Tests/PriorityRectTests.cs b/WindowTranslator.Tests/PriorityRectTests.cs new file mode 100644 index 00000000..e2970621 --- /dev/null +++ b/WindowTranslator.Tests/PriorityRectTests.cs @@ -0,0 +1,44 @@ +namespace WindowTranslator.Tests; + +/// +/// 優先矩形の座標計算に関するテスト +/// +public class PriorityRectTests +{ + [Fact] + public void ToAbsoluteRectは画像サイズに応じた絶対座標を返す() + { + var rect = new PriorityRect(0.25, 0.5, 0.25, 0.25); + + var abs = rect.ToAbsoluteRect(800, 600); + + Assert.Equal(200, abs.X); + Assert.Equal(300, abs.Y); + Assert.Equal(200, abs.Width); + Assert.Equal(150, abs.Height); + } + + [Fact] + public void FromAbsoluteRectは相対座標に変換する() + { + var rect = PriorityRect.FromAbsoluteRect(200, 300, 200, 150, 800, 600, "keyword"); + + Assert.Equal(0.25, rect.X); + Assert.Equal(0.5, rect.Y); + Assert.Equal(0.25, rect.Width); + Assert.Equal(0.25, rect.Height); + Assert.Equal("keyword", rect.Keyword); + } + + [Fact] + public void FromAbsoluteRectは小数の基準サイズを保持して相対座標に変換する() + { + var rect = PriorityRect.FromAbsoluteRect(400.4, 300.4, 200.2, 150.2, 800.8, 600.8); + + Assert.Equal(0.5, rect.X); + Assert.Equal(0.5, rect.Y); + Assert.Equal(0.25, rect.Width); + Assert.Equal(0.25, rect.Height); + } + +} diff --git a/WindowTranslator/Controls/PriorityRectsEditor.xaml b/WindowTranslator/Controls/PriorityRectsEditor.xaml new file mode 100644 index 00000000..20ed9755 --- /dev/null +++ b/WindowTranslator/Controls/PriorityRectsEditor.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/WindowTranslator/Controls/PriorityRectsEditor.xaml.cs b/WindowTranslator/Controls/PriorityRectsEditor.xaml.cs new file mode 100644 index 00000000..30634d2c --- /dev/null +++ b/WindowTranslator/Controls/PriorityRectsEditor.xaml.cs @@ -0,0 +1,244 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace WindowTranslator.Controls; + +/// +/// OCR対象範囲のリストを編集するコントロール +/// +public partial class PriorityRectsEditor : UserControl +{ + /// 編集対象のOCR対象範囲リスト + public IList? Rects + { + get => (IList?)GetValue(RectsProperty); + set => SetValue(RectsProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty RectsProperty = + DependencyProperty.Register(nameof(Rects), typeof(IList), typeof(PriorityRectsEditor), new PropertyMetadata(null, OnRectsChanged)); + + /// 矩形選択の対象となるウィンドウのハンドル + public nint TargetWindowHandle + { + get => (nint)GetValue(TargetWindowHandleProperty); + set => SetValue(TargetWindowHandleProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty TargetWindowHandleProperty = + DependencyProperty.Register(nameof(TargetWindowHandle), typeof(nint), typeof(PriorityRectsEditor), new PropertyMetadata(IntPtr.Zero, OnTargetWindowHandleChanged)); + + private readonly ObservableCollection items = []; + private RectangleSelectionWindow? previewWindow; + private bool isSyncing; + + public PriorityRectsEditor() + { + InitializeComponent(); + this.RectList.SetCurrentValue(ItemsControl.ItemsSourceProperty, this.items); + this.items.CollectionChanged += OnItemsChanged; + this.Unloaded += (_, _) => ClosePreview(); + UpdateButtonState(); + } + + private static void OnRectsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + => ((PriorityRectsEditor)d).LoadRects(e.NewValue as IList); + + private static void OnTargetWindowHandleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var editor = (PriorityRectsEditor)d; + editor.UpdateButtonState(); + editor.ShowSelectedRect(); + } + + private void LoadRects(IList? rects) + { + this.isSyncing = true; + try + { + foreach (var item in this.items) + { + item.PropertyChanged -= OnItemPropertyChanged; + } + this.items.Clear(); + foreach (var rect in rects ?? []) + { + var item = PriorityRectItem.From(rect); + item.PropertyChanged += OnItemPropertyChanged; + this.items.Add(item); + } + } + finally + { + this.isSyncing = false; + } + UpdateButtonState(); + ShowSelectedRect(); + } + + private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + foreach (var item in e.OldItems?.Cast() ?? []) + { + item.PropertyChanged -= OnItemPropertyChanged; + } + foreach (var item in e.NewItems?.Cast() ?? []) + { + item.PropertyChanged -= OnItemPropertyChanged; + item.PropertyChanged += OnItemPropertyChanged; + } + SyncToSource(); + UpdateButtonState(); + } + + private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) + => SyncToSource(); + + /// + /// 編集内容をへ反映する + /// + /// + /// 設定の保存はリストのインスタンスを参照するため、インスタンスを差し替えずに中身を書き換える + /// + private void SyncToSource() + { + if (this.isSyncing || Rects is not { } rects) + { + return; + } + rects.Clear(); + foreach (var item in this.items) + { + rects.Add(item.ToPriorityRect()); + } + } + + private void UpdateButtonState() + { + this.AddButton.SetCurrentValue(IsEnabledProperty, TargetWindowHandle != IntPtr.Zero); + this.AddButton.SetCurrentValue(ToolTipProperty, TargetWindowHandle != IntPtr.Zero ? null : Properties.Resources.PriorityRectTargetNotFound); + } + + private void RectList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + UpdateButtonState(); + ShowSelectedRect(); + } + + private void AddButton_Click(object sender, RoutedEventArgs e) + { + ClosePreview(); + var window = new RectangleSelectionWindow(TargetWindowHandle) { Owner = Window.GetWindow(this) }; + if (window.ShowDialog() == true && window.SelectedRect is { } rect) + { + this.items.Add(PriorityRectItem.From(rect)); + this.RectList.SetCurrentValue(Selector.SelectedIndexProperty, this.items.Count - 1); + } + else + { + ShowSelectedRect(); + } + } + + private void RemoveButton_Click(object sender, RoutedEventArgs e) + { + var item = (FrameworkElement)sender; + var index = this.items.IndexOf((PriorityRectItem)item.DataContext); + this.items.RemoveAt(index); + this.RectList.SetCurrentValue(Selector.SelectedIndexProperty, Math.Min(index, this.items.Count - 1)); + } + + private void ShowSelectedRect() + { + ClosePreview(); + if (TargetWindowHandle == IntPtr.Zero || this.RectList.SelectedItem is not PriorityRectItem item) + { + return; + } + + var window = new RectangleSelectionWindow(TargetWindowHandle, item.ToPriorityRect()); + this.previewWindow = window; + window.Closed += PreviewWindow_Closed; + window.Show(); + } + + private void ClosePreview() + { + if (this.previewWindow is not { } window) + { + return; + } + + this.previewWindow = null; + window.Closed -= PreviewWindow_Closed; + window.Close(); + } + + private void PreviewWindow_Closed(object? sender, EventArgs e) + { + if (ReferenceEquals(sender, this.previewWindow)) + { + this.previewWindow = null; + } + } +} + +/// +/// 編集中のOCR対象範囲 +/// +public sealed partial class PriorityRectItem : ObservableObject +{ + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayText))] + private double x; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayText))] + private double y; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayText))] + private double width; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayText))] + private double height; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayText))] + private string keyword = string.Empty; + + /// リストに表示する文字列 + public string DisplayText => $"({X:P1}, {Y:P1}) {Width:P1} x {Height:P1}" + + (string.IsNullOrWhiteSpace(Keyword) ? string.Empty : $" [{Keyword}]"); + + public static PriorityRectItem From(PriorityRect rect) + => new() { X = rect.X, Y = rect.Y, Width = rect.Width, Height = rect.Height, Keyword = rect.Keyword }; + + public PriorityRect ToPriorityRect() + => new(X, Y, Width, Height, Keyword); +} + +/// +/// 値がでないかどうかを表すへ変換する +/// +[ValueConversion(typeof(object), typeof(bool))] +public sealed class NotNullToBooleanConverter : IValueConverter +{ + public static NotNullToBooleanConverter Default { get; } = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is not null; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/WindowTranslator/Controls/RectangleSelectionWindow.xaml b/WindowTranslator/Controls/RectangleSelectionWindow.xaml new file mode 100644 index 00000000..41d0130e --- /dev/null +++ b/WindowTranslator/Controls/RectangleSelectionWindow.xaml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/WindowTranslator/Controls/RectangleSelectionWindow.xaml.cs b/WindowTranslator/Controls/RectangleSelectionWindow.xaml.cs new file mode 100644 index 00000000..dd4fe56b --- /dev/null +++ b/WindowTranslator/Controls/RectangleSelectionWindow.xaml.cs @@ -0,0 +1,229 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; +using WindowTranslator.Extensions; +using static Windows.Win32.PInvoke; + +namespace WindowTranslator.Controls; + +/// +/// 対象ウィンドウのクライアント領域に重ねて矩形を選択するウィンドウ +/// +public partial class RectangleSelectionWindow : Window +{ + /// + /// 選択された矩形として扱う最小の大きさ(クライアント領域に対する割合) + /// + private const double MinimumRelativeSize = 0.005; + + private readonly nint targetHandle; + private readonly PriorityRect? previewRect; + private Point startPoint; + private bool isSelecting; + + /// + /// 選択された矩形(クライアント領域に対する相対座標 0.0-1.0) + /// + public PriorityRect? SelectedRect { get; private set; } + + public RectangleSelectionWindow(nint targetHandle) + { + this.targetHandle = targetHandle; + InitializeComponent(); + } + + /// + /// 対象ウィンドウ上に指定したOCR対象範囲を表示する + /// + public RectangleSelectionWindow(nint targetHandle, PriorityRect previewRect) + : this(targetHandle) + { + this.previewRect = previewRect; + this.ShowActivated = false; + this.SelectionCanvas.SetCurrentValue(Panel.BackgroundProperty, Brushes.Transparent); + this.SelectionCanvas.SetCurrentValue(FrameworkElement.CursorProperty, Cursors.Arrow); + this.SelectionCanvas.SetCurrentValue(IsHitTestVisibleProperty, false); + this.InfoBorder.SetCurrentValue(VisibilityProperty, Visibility.Collapsed); + } + + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + if (!TryFitToCaptureArea()) + { + if (this.previewRect is null) + { + DialogResult = false; + } + Close(); + return; + } + + if (this.previewRect is not null) + { + var windowHandle = new HWND(new WindowInteropHelper(this).Handle); + var extendedStyle = (WINDOW_EX_STYLE)GetWindowLong(windowHandle, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE) + | WINDOW_EX_STYLE.WS_EX_NOACTIVATE + | WINDOW_EX_STYLE.WS_EX_TOOLWINDOW + | WINDOW_EX_STYLE.WS_EX_TRANSPARENT; + windowHandle.SetExtendedStyle(extendedStyle); + return; + } + + // 設定画面が対象ウィンドウに重なっていても選択範囲が見えるように、対象ウィンドウを前面に出す + SetWindowPos(new(this.targetHandle), HWND.Null, 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + Activate(); + } + + protected override void OnContentRendered(EventArgs e) + { + base.OnContentRendered(e); + if (this.previewRect is not { } rect) + { + return; + } + + Canvas.SetLeft(this.SelectionRect, rect.X * this.SelectionCanvas.ActualWidth); + Canvas.SetTop(this.SelectionRect, rect.Y * this.SelectionCanvas.ActualHeight); + this.SelectionRect.SetCurrentValue(WidthProperty, rect.Width * this.SelectionCanvas.ActualWidth); + this.SelectionRect.SetCurrentValue(HeightProperty, rect.Height * this.SelectionCanvas.ActualHeight); + this.SelectionRect.SetCurrentValue(VisibilityProperty, Visibility.Visible); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Key == Key.Escape) + { + DialogResult = false; + Close(); + } + } + + /// + /// キャプチャ画像と同じ範囲になるようにウィンドウを配置する + /// + /// + /// キャプチャ画像はウィンドウ全体のフレームから + /// の上端との左右下端で切り出した範囲になるため、 + /// と同じ計算で位置と大きさを求める + /// + /// 配置できた場合は + private bool TryFitToCaptureArea() + { + var windowInfo = new WINDOWINFO() { cbSize = (uint)Marshal.SizeOf() }; + if (this.targetHandle == IntPtr.Zero || !GetWindowInfo(new(this.targetHandle), ref windowInfo)) + { + return false; + } + + var client = windowInfo.rcClient; + var left = client.left; + var top = windowInfo.rcWindow.top; + var placement = default(WINDOWPLACEMENT); + // 最大化時はウィンドウの上端が画面外に出るため、作業領域の上端を使う + if (GetWindowPlacement(new(this.targetHandle), ref placement) && placement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_MAXIMIZE)) + { + var monitor = MonitorFromWindow(new(this.targetHandle), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + var monitorInfo = default(MONITORINFOEXW); + monitorInfo.monitorInfo.cbSize = (uint)Marshal.SizeOf(); + if (GetMonitorInfo(monitor, ref monitorInfo.monitorInfo)) + { + top = monitorInfo.monitorInfo.rcWork.top; + } + } + + var width = client.right - left; + var height = client.bottom - top; + if (width <= 0 || height <= 0) + { + return false; + } + + // Win32のスクリーン座標(物理ピクセル)をWPFの座標(DIP)に変換する + var dpiScale = GetDpiForSystem() / 96.0; + SetCurrentValue(LeftProperty, left / dpiScale); + SetCurrentValue(TopProperty, top / dpiScale); + SetCurrentValue(WidthProperty, width / dpiScale); + SetCurrentValue(HeightProperty, height / dpiScale); + return true; + } + + private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + this.startPoint = e.GetPosition(this.SelectionCanvas); + this.isSelecting = true; + this.SelectionCanvas.CaptureMouse(); + this.SelectionRect.SetCurrentValue(VisibilityProperty, Visibility.Visible); + UpdateSelectionRect(this.startPoint); + } + + private void Canvas_MouseMove(object sender, MouseEventArgs e) + { + if (!this.isSelecting) + { + return; + } + + UpdateSelectionRect(e.GetPosition(this.SelectionCanvas)); + } + + private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (!this.isSelecting) + { + return; + } + + this.isSelecting = false; + this.SelectionCanvas.ReleaseMouseCapture(); + + var canvasWidth = this.SelectionCanvas.ActualWidth; + var canvasHeight = this.SelectionCanvas.ActualHeight; + if (canvasWidth <= 0 || canvasHeight <= 0) + { + return; + } + + var rect = PriorityRect.FromAbsoluteRect( + Canvas.GetLeft(this.SelectionRect), + Canvas.GetTop(this.SelectionRect), + this.SelectionRect.Width, + this.SelectionRect.Height, + canvasWidth, + canvasHeight); + + // 誤クリックによる極端に小さい矩形は選択し直してもらう + if (rect.Width < MinimumRelativeSize || rect.Height < MinimumRelativeSize) + { + this.SelectionRect.SetCurrentValue(VisibilityProperty, Visibility.Collapsed); + this.InfoText.SetCurrentValue(TextBlock.TextProperty, Properties.Resources.PriorityRectTooSmall); + return; + } + + this.SelectedRect = rect; + DialogResult = true; + Close(); + } + + private void UpdateSelectionRect(Point currentPoint) + { + var x = Math.Clamp(Math.Min(this.startPoint.X, currentPoint.X), 0, this.SelectionCanvas.ActualWidth); + var y = Math.Clamp(Math.Min(this.startPoint.Y, currentPoint.Y), 0, this.SelectionCanvas.ActualHeight); + var width = Math.Clamp(Math.Max(this.startPoint.X, currentPoint.X), 0, this.SelectionCanvas.ActualWidth) - x; + var height = Math.Clamp(Math.Max(this.startPoint.Y, currentPoint.Y), 0, this.SelectionCanvas.ActualHeight) - y; + + Canvas.SetLeft(this.SelectionRect, x); + Canvas.SetTop(this.SelectionRect, y); + this.SelectionRect.SetCurrentValue(WidthProperty, width); + this.SelectionRect.SetCurrentValue(HeightProperty, height); + this.InfoText.SetCurrentValue(TextBlock.TextProperty, $"{Properties.Resources.PriorityRectSelecting}: ({x:F0}, {y:F0}) - ({width:F0} x {height:F0})"); + } +} diff --git a/WindowTranslator/Modules/Main/MainViewModelBase.cs b/WindowTranslator/Modules/Main/MainViewModelBase.cs index a83bbc4a..f104450d 100644 --- a/WindowTranslator/Modules/Main/MainViewModelBase.cs +++ b/WindowTranslator/Modules/Main/MainViewModelBase.cs @@ -24,6 +24,7 @@ public abstract partial class MainViewModelBase : IDisposable { private readonly Timer timer; private readonly IOcrModule ocr; + private readonly List priorityRects; private readonly IOcrTextTracker ocrTextTracker; private readonly ITranslateModule translator; private readonly ICacheModule cache; @@ -71,6 +72,7 @@ public MainViewModelBase( IProcessInfoStore processInfoStore, ICaptureModule capture, IOcrModule ocr, + IOptionsSnapshot ocrParam, IOcrTextTracker ocrTextTracker, ITranslateModule translator, ICacheModule cache, @@ -89,6 +91,7 @@ public MainViewModelBase( this.capture = capture ?? throw new ArgumentNullException(nameof(capture)); this.capture.Captured += Capture_CapturedAsync; this.ocr = ocr ?? throw new ArgumentNullException(nameof(ocr)); + this.priorityRects = ocrParam.Value.PriorityRects ?? []; this.ocrTextTracker = ocrTextTracker ?? throw new ArgumentNullException(nameof(ocrTextTracker)); this.translator = translator ?? throw new ArgumentNullException(nameof(translator)); this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); @@ -170,7 +173,36 @@ private async Task CreateTextOverlayAsync() { try { - texts = await this.ocr.RecognizeAsync(sbmp); + var regions = new List(); + if (this.priorityRects.Count == 0) + { + regions.Add(new(new(0, 0, sbmp.PixelWidth, sbmp.PixelHeight))); + } + else + { + foreach (var priorityRect in this.priorityRects) + { + var rect = priorityRect.ToAbsoluteRect(sbmp.PixelWidth, sbmp.PixelHeight); + var left = Math.Clamp(rect.Left, 0, sbmp.PixelWidth); + var top = Math.Clamp(rect.Top, 0, sbmp.PixelHeight); + var right = Math.Clamp(rect.Right, 0, sbmp.PixelWidth); + var bottom = Math.Clamp(rect.Bottom, 0, sbmp.PixelHeight); + if (right - left < 1 || bottom - top < 1) + { + continue; + } + + var pixelLeft = Math.Floor(left); + var pixelTop = Math.Floor(top); + var pixelRight = Math.Ceiling(right); + var pixelBottom = Math.Ceiling(bottom); + regions.Add(new( + new(pixelLeft, pixelTop, pixelRight - pixelLeft, pixelBottom - pixelTop), + priorityRect.Keyword)); + } + } + + texts = await this.ocr.RecognizeAsync(new(sbmp, regions)); texts = this.ocrTextTracker.Update(texts, new(sbmp.PixelWidth, sbmp.PixelHeight)); } catch (ObjectDisposedException) @@ -332,13 +364,14 @@ public sealed class CaptureMainViewModel( [Inject] IProcessInfoStore processInfoStore, [Inject] ICaptureModule capture, [Inject] IOcrModule ocr, + [Inject] IOptionsSnapshot ocrParam, [Inject] IOcrTextTracker ocrTextTracker, [Inject] ITranslateModule translator, [Inject] ICacheModule cache, [Inject] IColorModule color, [Inject] IEnumerable filters, [Inject] ILogger logger) - : MainViewModelBase(presentationService, options, processInfoStore, capture, ocr, ocrTextTracker, translator, cache, color, filters, logger) + : MainViewModelBase(presentationService, options, processInfoStore, capture, ocr, ocrParam, ocrTextTracker, translator, cache, color, filters, logger) { public ICaptureModule Capture { get; } = capture ?? throw new ArgumentNullException(nameof(capture)); } @@ -350,12 +383,13 @@ public sealed class OverlayMainViewModel( [Inject] IProcessInfoStore processInfoStore, [Inject] ICaptureModule capture, [Inject] IOcrModule ocr, + [Inject] IOptionsSnapshot ocrParam, [Inject] IOcrTextTracker ocrTextTracker, [Inject] ITranslateModule translator, [Inject] ICacheModule cache, [Inject] IColorModule color, [Inject] IEnumerable filters, [Inject] ILogger logger) - : MainViewModelBase(presentationService, options, processInfoStore, capture, ocr, ocrTextTracker, translator, cache, color, filters, logger) + : MainViewModelBase(presentationService, options, processInfoStore, capture, ocr, ocrParam, ocrTextTracker, translator, cache, color, filters, logger) { } diff --git a/WindowTranslator/Modules/Ocr/WindowsMediaOcr.cs b/WindowTranslator/Modules/Ocr/WindowsMediaOcr.cs index 11c47fa6..f62ffe86 100644 --- a/WindowTranslator/Modules/Ocr/WindowsMediaOcr.cs +++ b/WindowTranslator/Modules/Ocr/WindowsMediaOcr.cs @@ -38,32 +38,34 @@ public sealed partial class WindowsMediaOcr( private readonly InMemoryRandomAccessStream resizeStream = new(); private readonly CancellationTokenSource cts = new(); - public async ValueTask> RecognizeAsync(SoftwareBitmap bitmap) + public ValueTask> RecognizeAsync(OcrCaptureInput input) { - var newWidth = (uint)(bitmap.PixelWidth * scale); - var newHeight = (uint)(bitmap.PixelHeight * scale); - if (newWidth > OcrEngine.MaxImageDimension || newHeight > OcrEngine.MaxImageDimension) + foreach (var region in input.Regions) { - throw new AppUserException($"ウィンドウサイズが大きすぎます。対象ウィンドウのサイズを小さくするか、認識設定の拡大率を下げてください。actual:({newWidth},{newHeight}), max:{OcrEngine.MaxImageDimension}"); - } - - // リサイズ処理(scale != 1.0 の場合は新しいビットマップを生成) - var workingBitmap = await bitmap.ResizeSoftwareBitmapAsync(this.scale, this.cts.Token); - this.cts.Token.ThrowIfCancellationRequested(); - - // 明るさ・コントラスト調整(インプレース) - // scale == 1.0 の場合はリサイズで元のビットマップが返るため、コピーを作成してから調整 - if (this.brightness != 0 || this.contrast != 0) - { - if (workingBitmap == bitmap) + var width = (uint)(region.Bounds.Width * this.scale); + var height = (uint)(region.Bounds.Height * this.scale); + if (width > OcrEngine.MaxImageDimension || height > OcrEngine.MaxImageDimension) { - // 元のビットマップを変更しないようにコピーを作成 - workingBitmap = SoftwareBitmap.Copy(bitmap); + throw new AppUserException($"ウィンドウサイズが大きすぎます。対象ウィンドウのサイズを小さくするか、認識設定の拡大率を下げてください。actual:({width},{height}), max:{OcrEngine.MaxImageDimension}"); } - workingBitmap.AdjustBrightnessContrastInPlace(this.brightness, this.contrast); } - this.cts.Token.ThrowIfCancellationRequested(); + return OcrUtility.RecognizeRegionsAsync( + input, + RecognizeRegionAsync, + this.scale, + this.brightness, + this.contrast, + this.cts.Token); + } + + /// + /// 指定した画像のテキストを認識する + /// + /// 認識対象の画像 + /// 拡大後の全体画像サイズ + private async ValueTask> RecognizeRegionAsync(SoftwareBitmap workingBitmap, System.Drawing.Size sourceSize) + { var t = this.logger.LogDebugTime("OCR Recognize"); var rawResults = await ocr.RecognizeAsync(workingBitmap); this.cts.Token.ThrowIfCancellationRequested(); @@ -95,7 +97,7 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm .Lines .Select(line => CalcRect(line, angle, centerX, centerY)) // 大きすぎる文字は映像の認識ミスとみなす - .Where(w => w.Height < workingBitmap.PixelHeight * 0.1) + .Where(w => w.Height < sourceSize.Height * 0.1) .ToArray(); if (lineResults.IsEmpty()) @@ -103,8 +105,8 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm return lineResults; } - var xt = xPosThrethold * workingBitmap.PixelWidth; - var yt = yPosThrethold * workingBitmap.PixelHeight; + var xt = xPosThrethold * sourceSize.Width; + var yt = yPosThrethold * sourceSize.Height; var results = new List(lineResults.Length); { @@ -140,12 +142,7 @@ public async ValueTask> RecognizeAsync(SoftwareBitmap bitm } } - if (bitmap != workingBitmap) - { - workingBitmap.Dispose(); - } - - return results.Select(r => ToTextRect(r, this.scale, angle)) + return results.Select(r => ToTextRect(r, angle)) // マージ後に少なすぎる文字も認識ミス扱い // 特殊なグリフの言語は対象外(日本語、中国語、韓国語、ロシア語) .Where(w => IsSpecialLang(this.source) || w.SourceText.Length > 2) @@ -288,16 +285,10 @@ public TextRect ToRect() } } - private static TextRect ToTextRect(TempMergeRect combinedRect, double scale, double angle) + private static TextRect ToTextRect(TempMergeRect combinedRect, double angle) { var (x, y, width, height, fontSize, _) = combinedRect; var text = combinedRect.Text; - // 元の画像座標に変換 - x /= scale; - y /= scale; - width /= scale; - height /= scale; - fontSize /= scale; // 高さがフォントサイズの2倍以上の場合は複数行とみなす // または、 // スペース言語の場合は単語数が2以上、それ以外の場合は文字数が8文字以上の場合は複数行とみなす(やっぱり微妙…) @@ -311,7 +302,7 @@ private static TextRect ToTextRect(TempMergeRect combinedRect, double scale, dou height += fontSize * fat; y -= fontSize * fat * .5; - return new(text, x, y, width, height, fontSize, lines) { Angle = angle }; + return new TextRect(text, x, y, width, height, fontSize, lines) { Angle = angle }; } private TextRect CalcRect(OcrLine line, double angle, double centerX, double centerY) @@ -462,4 +453,4 @@ private static bool IsAllSameChar(string text) ReadOnlySpan chars = text; return !chars[1..].ContainsAnyExcept(chars[0]); } -} \ No newline at end of file +} diff --git a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs index 3a7002cd..c9e43488 100644 --- a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs +++ b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs @@ -370,6 +370,13 @@ public partial class TargetSettingsViewModel( [Browsable(false)] public string Name { get; } = name; + /// + /// 対象ウィンドウのハンドル(翻訳中でない場合は) + /// + [Browsable(false)] + public nint TargetWindowHandle + => sp.GetService()?.OpenedWindows.FirstOrDefault(w => w.Name == Name)?.Target ?? IntPtr.Zero; + [Browsable(false)] public IEnumerable OcrModules { get; } = ocrModules; [Browsable(false)] diff --git a/WindowTranslator/Modules/Settings/SettingsPropertyGridFactory.cs b/WindowTranslator/Modules/Settings/SettingsPropertyGridFactory.cs index 9d37f725..c370c4d5 100644 --- a/WindowTranslator/Modules/Settings/SettingsPropertyGridFactory.cs +++ b/WindowTranslator/Modules/Settings/SettingsPropertyGridFactory.cs @@ -45,6 +45,15 @@ public override FrameworkElement CreateControl(PropertyItem property, PropertyCo fe.SetBinding(TextBox.TextProperty, property.CreateBinding()); } + // OCR対象範囲は専用のエディタで編集する + if (property.Is(typeof(List))) + { + var editor = new PriorityRectsEditor(); + editor.SetBinding(PriorityRectsEditor.RectsProperty, property.CreateOneWayBinding()); + editor.SetBinding(PriorityRectsEditor.TargetWindowHandleProperty, new Binding(nameof(TargetSettingsViewModel.TargetWindowHandle))); + fe = editor; + } + // EditableItemsSourceAttributeが指定されている場合、編集可能ComboBoxを生成 if (fe == null && property is IEditableItemsPropertyItem editableItem && editableItem.EditableCandidates != null) { diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs index 5baefe6e..a66df7da 100644 --- a/WindowTranslator/Properties/Resources.Designer.cs +++ b/WindowTranslator/Properties/Resources.Designer.cs @@ -417,6 +417,46 @@ internal Resources() { /// public static string Plugin => ResourceManager.GetString("Plugin", resourceCulture) ?? string.Empty; + /// + /// "範囲追加" に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectAdd => ResourceManager.GetString("PriorityRectAdd", resourceCulture) ?? string.Empty; + + /// + /// "キーワード" に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectKeyword => ResourceManager.GetString("PriorityRectKeyword", resourceCulture) ?? string.Empty; + + /// + /// "コンテキストが有効な翻訳モジュールで使用されます" に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectKeywordDescription => ResourceManager.GetString("PriorityRectKeywordDescription", resourceCulture) ?? string.Empty; + + /// + /// "選択中" に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectSelecting => ResourceManager.GetString("PriorityRectSelecting", resourceCulture) ?? string.Empty; + + /// + /// "矩形選択" に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectSelection => ResourceManager.GetString("PriorityRectSelection", resourceCulture) ?? string.Empty; + + /// + /// "ドラッグして矩形を選択してください(Escキーでキャンセル)&#13;&#10;文字..." に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectSelectionGuide => ResourceManager.GetString("PriorityRectSelectionGuide", resourceCulture) ?? string.Empty; + + /// + /// "翻訳中のウィンドウがないため矩形を選択できません。対象ウィンドウの翻訳を開始してから設定してください..." に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectTargetNotFound => ResourceManager.GetString("PriorityRectTargetNotFound", resourceCulture) ?? string.Empty; + + /// + /// "矩形が小さすぎます。もう一度選択してください。" に類似しているローカライズされた文字列を検索します。 + /// + public static string PriorityRectTooSmall => ResourceManager.GetString("PriorityRectTooSmall", resourceCulture) ?? string.Empty; + /// /// "公開ページ" に類似しているローカライズされた文字列を検索します。 /// diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx index 840afd77..3ac1b885 100644 --- a/WindowTranslator/Properties/Resources.ar.resx +++ b/WindowTranslator/Properties/Resources.ar.resx @@ -447,4 +447,10 @@ + + إضافة نطاق + + + تستخدمه وحدات الترجمة التي تدعم السياق + diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx index 31d903ef..bea5144a 100644 --- a/WindowTranslator/Properties/Resources.cs.resx +++ b/WindowTranslator/Properties/Resources.cs.resx @@ -337,4 +337,10 @@ Monitory nejsou podporovány. + + Přidat oblast + + + Používají jej překladové moduly podporující kontext + diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx index 1b51a657..80a24e2f 100644 --- a/WindowTranslator/Properties/Resources.de.resx +++ b/WindowTranslator/Properties/Resources.de.resx @@ -456,4 +456,10 @@ Monitore werden nicht unterstützt. + + Bereich hinzufügen + + + Wird von Übersetzungsmodulen verwendet, die Kontext unterstützen + diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx index c8c80457..74b34056 100644 --- a/WindowTranslator/Properties/Resources.en.resx +++ b/WindowTranslator/Properties/Resources.en.resx @@ -456,4 +456,29 @@ Monitors are not supported. + + Add range + + + Keyword + + + Used by translation modules that support context + + + Selecting + + + Rectangle Selection + + + Drag to select a rectangle (press Esc to cancel) +Select a slightly wider area so that text is not cut off + + + No window is being translated, so a rectangle cannot be selected. Start translating the target window before configuring. + + + The rectangle is too small. Please select again. + diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx index f3388633..c77ef136 100644 --- a/WindowTranslator/Properties/Resources.es.resx +++ b/WindowTranslator/Properties/Resources.es.resx @@ -447,4 +447,10 @@ + + Añadir área + + + Se utiliza en los módulos de traducción compatibles con el contexto + diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx index a961b79f..a8ca5919 100644 --- a/WindowTranslator/Properties/Resources.fa.resx +++ b/WindowTranslator/Properties/Resources.fa.resx @@ -441,4 +441,10 @@ + + افزودن محدوده + + + توسط ماژول‌های ترجمه‌ای که از زمینه پشتیبانی می‌کنند استفاده می‌شود + diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx index d5f18cee..b2832a2a 100644 --- a/WindowTranslator/Properties/Resources.fil.resx +++ b/WindowTranslator/Properties/Resources.fil.resx @@ -456,4 +456,10 @@ Ang monitor ay hindi suportado. + + Magdagdag ng saklaw + + + Ginagamit ng mga module ng pagsasalin na sumusuporta sa konteksto + diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx index 028dda7c..cb4ee0de 100644 --- a/WindowTranslator/Properties/Resources.fr.resx +++ b/WindowTranslator/Properties/Resources.fr.resx @@ -447,4 +447,10 @@ + + Ajouter une zone + + + Utilisé par les modules de traduction prenant en charge le contexte + diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx index b38b8fe1..55158965 100644 --- a/WindowTranslator/Properties/Resources.hi.resx +++ b/WindowTranslator/Properties/Resources.hi.resx @@ -449,4 +449,10 @@ - \ No newline at end of file + + क्षेत्र जोड़ें + + + संदर्भ का समर्थन करने वाले अनुवाद मॉड्यूल द्वारा उपयोग किया जाता है + + diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx index 79ab42f1..e2ce62ef 100644 --- a/WindowTranslator/Properties/Resources.hu.resx +++ b/WindowTranslator/Properties/Resources.hu.resx @@ -337,4 +337,10 @@ A monitorok nem támogatottak. + + Tartomány hozzáadása + + + A kontextust támogató fordítási modulok használják + diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx index 626bdbaa..615464ab 100644 --- a/WindowTranslator/Properties/Resources.id.resx +++ b/WindowTranslator/Properties/Resources.id.resx @@ -455,4 +455,10 @@ Monitor tidak didukung. + + Tambahkan area + + + Digunakan oleh modul terjemahan yang mendukung konteks + diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx index 72483a3f..bfc9746b 100644 --- a/WindowTranslator/Properties/Resources.ko.resx +++ b/WindowTranslator/Properties/Resources.ko.resx @@ -456,4 +456,10 @@ + + 범위 추가 + + + 컨텍스트를 지원하는 번역 모듈에서 사용됩니다 + diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx index 481b193e..e885892e 100644 --- a/WindowTranslator/Properties/Resources.ms.resx +++ b/WindowTranslator/Properties/Resources.ms.resx @@ -455,4 +455,10 @@ Monitor tidak disokong. + + Tambah kawasan + + + Digunakan oleh modul terjemahan yang menyokong konteks + diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx index 576d7a95..995179aa 100644 --- a/WindowTranslator/Properties/Resources.pl.resx +++ b/WindowTranslator/Properties/Resources.pl.resx @@ -456,4 +456,10 @@ Monitory nie są obsługiwane. + + Dodaj obszar + + + Używane przez moduły tłumaczeniowe obsługujące kontekst + diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx index 0a3b952d..b9cd7407 100644 --- a/WindowTranslator/Properties/Resources.pt-BR.resx +++ b/WindowTranslator/Properties/Resources.pt-BR.resx @@ -455,4 +455,10 @@ Monitor tidak didukung. + + Adicionar área + + + Usado por módulos de tradução compatíveis com contexto + diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx index 29f224b1..05b1d716 100644 --- a/WindowTranslator/Properties/Resources.resx +++ b/WindowTranslator/Properties/Resources.resx @@ -144,6 +144,31 @@ プラグイン設定 + + 範囲追加 + + + キーワード + + + コンテキストが有効な翻訳モジュールで使用されます + + + 選択中 + + + 矩形選択 + + + ドラッグして矩形を選択してください(Escキーでキャンセル) +文字が途中で切れないように少し広めに囲んでください + + + 翻訳中のウィンドウがないため矩形を選択できません。対象ウィンドウの翻訳を開始してから設定してください。 + + + 矩形が小さすぎます。もう一度選択してください。 + 言語設定 diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx index 701b47b9..3d9758b5 100644 --- a/WindowTranslator/Properties/Resources.ru.resx +++ b/WindowTranslator/Properties/Resources.ru.resx @@ -447,4 +447,10 @@ + + Добавить область + + + Используется модулями перевода, поддерживающими контекст + diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx index 78d60f9a..d747edc0 100644 --- a/WindowTranslator/Properties/Resources.th.resx +++ b/WindowTranslator/Properties/Resources.th.resx @@ -456,4 +456,10 @@ + + เพิ่มพื้นที่ + + + ใช้โดยโมดูลการแปลที่รองรับบริบท + diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx index e8cfdbd1..94efd9fb 100644 --- a/WindowTranslator/Properties/Resources.tr.resx +++ b/WindowTranslator/Properties/Resources.tr.resx @@ -456,4 +456,10 @@ Monitör desteklenmiyor. + + Alan ekle + + + Bağlamı destekleyen çeviri modülleri tarafından kullanılır + diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx index 469d378c..aaa64e0c 100644 --- a/WindowTranslator/Properties/Resources.vi.resx +++ b/WindowTranslator/Properties/Resources.vi.resx @@ -456,4 +456,10 @@ Màn hình không được hỗ trợ. + + Thêm vùng + + + Được sử dụng bởi các mô-đun dịch hỗ trợ ngữ cảnh + diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx index 2af7736c..c46b6a0a 100644 --- a/WindowTranslator/Properties/Resources.zh-CN.resx +++ b/WindowTranslator/Properties/Resources.zh-CN.resx @@ -456,4 +456,10 @@ + + 添加范围 + + + 由支持上下文的翻译模块使用 + diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx index 25b11251..a412f50c 100644 --- a/WindowTranslator/Properties/Resources.zh-TW.resx +++ b/WindowTranslator/Properties/Resources.zh-TW.resx @@ -456,4 +456,10 @@ + + 新增範圍 + + + 供支援上下文的翻譯模組使用 + diff --git a/WindowTranslator/WindowTranslator.csproj b/WindowTranslator/WindowTranslator.csproj index a248f527..6cf172b0 100644 --- a/WindowTranslator/WindowTranslator.csproj +++ b/WindowTranslator/WindowTranslator.csproj @@ -38,6 +38,7 @@ + all