src/docs/flow-image-translation.md
Smart 分段回退策略。STranslate/ViewModels/ImageTranslateWindowViewModel.cs
ExecuteAsync(Bitmap):图片翻译窗口主执行命令。ApplyLayoutAnalysis(OcrResult):按分段模式生成 OcrLayoutBlock。ImageTranslateRenderer.CreateTranslatedOverlay(...):生成译文矢量绘制文档和原图坐标系中的选择框。RefreshSelectableOcrWords():在原文标注图和矢量译文视图之间切换图片文本选中数据源。STranslate/Views/ImageTranslateWindow.xaml / ImageTranslateCompactWindow.xaml
Standalone:原独立窗口,保留服务、语言、文本框和完整工具栏。Compact:无标题精简窗口,图片区贴回截图选区,底部预留悬浮核心按钮区。STranslate/Core/Screenshot.cs
GetScreenshotCaptureAsync():调用 ScreenGrabber.CaptureWithRegionAsync,截图时直接回传选区物理坐标,无需事后反推。padImage: false,关闭 ScreenGrab 对 <64px 小截图的背景画布 padding 扩展,保证 bitmap.Size == 选区物理尺寸,避免贴回时 Viewbox 把 padding 图缩放导致原始内容缩小;其他窗口模式保留默认 padding 行为。STranslate/Core/OcrLayoutAnalyzer.cs
AnalyzeBlocks(OcrResult, LayoutAnalysisMode):分段逻辑入口。Auto / Provider / Smart / NoMerge:图片翻译分段策略。STranslate/Core/OcrLayoutBlock.cs
STranslate/Core/ImageTranslateTextOverlayLayout.cs
STranslate/Services/OcrService.cs
GetImageTranslateOcrServices() / GetImageTranslateOcrServiceOrDefault():图片翻译 OCR 服务筛选和兜底。STranslate/Services/TranslateService.cs
ImageTranslateService:图片翻译专用翻译服务。STranslate.Plugin/IOcrPlugin.cs
IOcrPlugin.SupportBoxPoints()、OcrRequest、OcrResult、BoxPoint。IScreenshot.GetScreenshotCaptureAsync() 调用 ScreenGrabber.CaptureWithRegionAsync,截图时即回传选区物理坐标;精简窗口优先按该坐标贴回选区,仅异常情况下回退到光标附近定位。Settings.ImageTranslateWindowMode 决定使用 ImageTranslateWindow 还是 ImageTranslateCompactWindow 承载同一个 ImageTranslateWindowViewModel。ImageTranslateWindowViewModel.ExecuteAsync(bitmap) 清空旧状态,缓存 _sourceImage 并显示原图。OcrService.GetImageTranslateOcrSvcOrDefault()。候选服务必须实现 IOcrPlugin,并让 SupportBoxPoints() 返回 true。OcrRequest(data, Settings.OcrLanguage, bitmap.Width, bitmap.Height),插件必须返回图片像素坐标 BoxPoints。Utilities.PrepareOcrResult();如果插件只填充结构化 Regions,宿主会投影出兼容的 OcrContents。OcrWordBuilder.CreateFromOcrContents(_lastOcrResult.OcrContents) 生成原文选中块。ApplyLayoutAnalysis() 生成 OcrLayoutBlock,并把分析后的块投影回 OcrResult.OcrContents,供标注图、复制和结果文本复用。TranslateService.ImageTranslateService,该服务必须是 ITranslatePlugin,词典类服务不会进入图片翻译翻译列表。OcrLayoutBlock.Text 并发执行语言检测和翻译;翻译成功后用 ImageTranslateTextOverlayLayout.NormalizeOverlayText() 收敛空白,再回写到对应 block。ImageTranslateOverlayDocument:每个绘制项保存覆盖背景、裁剪范围、阴影和 FormattedText;选择框保持原图像素坐标,不再创建超采样结果位图。ImageZoom 在同一个 Viewbox 内按“原图 → 矢量译文 Overlay → 文字选择高亮”绘制,图片缩放、拖动和 DPI 变换会同步作用于三层。Settings.IsImTranShowingAnnotated 控制显示标注图还是原图加译文 Overlay;图片文本选中同步切换为原文块或译文块。Standalone 是默认模式,保留当前可缩放、可调整大小的独立窗口。Compact 使用无标题、不可缩放、非任务栏、完全透明窗口,窗口本身无背景色;屏幕上只看到截图内容 + 悬浮按钮条(按钮条自带半透明胶囊背景)。ImageTranslateCompactWindowPlacement.CreateLayout,返回窗口矩形、图片偏移、按钮条位置与 ToolbarSide(Below/Above/Overlay),由 ImageTranslateCompactWindow.ApplyLayoutToVisualTree 换算成 DIP 应用到 ImageZoom 与按钮条 Border。Esc、点击窗口外部或再次触发图片翻译关闭;右键菜单和窗口内部文字选择不会触发外部关闭。Settings.IsImTranShowingTextControl 只影响独立窗口。Auto:默认模式。OCR 返回结构化 Regions 时使用 Provider 段落;没有结构化分段时回退 Smart。Provider:只使用服务商结构化 Regions -> Paragraphs -> Lines;缺失结构化分段时退化为 NoMerge,不自行猜段落。Smart:本地智能分段。适用于只返回扁平 OcrContents 但有坐标框的 OCR。NoMerge:保留 OCR 原始块,适合用户希望逐块翻译或服务商块已经足够稳定的场景。Smart 只在宿主内部生效,不改变插件接口和外部枚举。
BuildLineSegments() 先按 Y 位置恢复视觉行,并按横向间距拆成行内 segment。File Explorer Add-ons File Locksmith 这类同一行跨单元格误合并。BuildLayoutRegions() 按列/区域相似度聚合,避免不同栏之间链式吞并。AnalyzeRegion() 在 region 内合并 paragraph;普通段落、PDF 连续行、多列正文和英文断词续行继续使用原段落合并规则。TableLike:至少多行、多列、多个视觉行有横向 peers,并且列左边缘或中心点在多行重复对齐。TableLike region 内禁止跨视觉行继续追加为同一 paragraph,所以功能列表、表格单元格默认按每个视觉行/单元项独立翻译。图片翻译对 OCR 插件的要求比普通 OCR 更高:
IOcrPlugin.SupportBoxPoints() 并返回 true。BoxPoints。OcrResult.OcrContents,宿主会用 Smart 分段。OcrResult.Regions。paragraph=true 获取 paragraphs_result.words_result_idx,再把对应 words_result 行组装成 OcrRegion -> OcrParagraph -> OcrContent。OcrResult.OcrContents 仍是兼容旧插件和旧调用链的扁平结果;结构化插件可以同时填充它,也可以只填充 Regions。OcrRequest.PixelWidth / PixelHeight 换算成图片像素坐标后再写入 BoxPoints。Auto 模式下,结构化 OCR 的 Provider 段落优先级高于本地 Smart,所以插件返回的 Regions 会直接影响分段粒度。插件侧应尽量让 Paragraphs 表示真实语义段落或表格单元项,而不是把整列/整表合成一个 paragraph。
ImageZoom.Source,由单个 retained-mode ImageTranslateOverlay 控件绘制全部译文;不为每个文本块创建独立 WPF 控件,也不生成常驻结果位图。OcrLayoutBlock.LineBoxPoints,尽量只擦除原文行;缺少行框时退回 block 外接框。3.2 倍。1.24 × 字号 作为基础行高,字号测量与最终绘制使用相同规则,不区分译文语言。2.0 × 字号;仍有余量时整体垂直居中。ImageZoom 使用 OcrWords 模拟图片上的文本选中。OcrWords 来自原始 OCR 坐标和原文文本。OcrWords 来自矢量文档中的译文字符框,复制所选内容时拿到的是译文。ServiceSettings.ImageTranslateOcrSvcID,由图片翻译窗口的 OCR 选择写入。ServiceSettings.ImageTranslateSvcID,由图片翻译窗口的翻译服务选择写入。Settings.ImageTranslateWindowMode 控制图片翻译结果使用独立窗口或精简窗口,默认 Standalone。Settings.LayoutAnalysisMode 是分段模式配置,默认 Auto,序列化支持 auto、provider、smart、noMerge;旧未知值归一为 Auto。Settings.IsImTranShowingAnnotated 控制标注图/原图加矢量译文 Overlay 显示。Settings.IsImTranShowingTextControl 控制图片翻译窗口文本区域显示。Settings.ImageTranslateSourceLang / ImageTranslateTargetLang 控制图片翻译语言。Settings.ShowImageTranslateItemInNotifyIconMenu 控制托盘菜单是否显示图片翻译入口。Helper.PromptConfigureService() 弹出配置提示并定位到 OcrPage。_snackbar.ShowWarning("NoTranslateService")。ImageTranslateWindowViewModel logger。LanguageDetectionFailed。TaskCanceledException,当前实现不额外弹提示。图片翻译窗口的内存释放主要涉及窗口视觉树、消息提示控件和 ViewModel 所有权。精简窗口与独立窗口都使用独立 DI scope,并在关闭时显式拆解各自的长生命周期持有链:
窗口视觉树泄漏(已修复):精简窗口每次截图翻译都新建并 OnDeactivated → Close() 关闭。旧版 ui:InfoBar 会被 WPF 静态 System.ComponentModel.PropertyDescriptor._propertyMap 通过 MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor 注册 PropertyChangeTracker,窗口关闭后反向钉死整窗及其 BitmapSource 原生帧缓冲。
SnackbarContainer、图片翻译/OCR 窗口的内联提示和热键冲突提示统一改用纯 WPF NoticeBar,应用代码不再创建 iNKORE InfoBar。SnackbarBehavior 预挂载;全局 Snackbar 首次向某个窗口显示消息时才把一个 SnackbarContainer 加入该窗口,并在 Window.Closed 时从视觉树移除并 Dispose()。SnackbarContainer.Dispose() 幂等停止自动隐藏计时器和可控 Storyboard,解绑 NoticeBar 事件并清除 action callback;释放后再次 Show() 会抛出 ObjectDisposedException。ImageTranslateCompactWindow.OnClosed 在释放独立 DI scope 前继续调用 DetachVisualTree(),清除子元素、Content 和 DataContext,避免后续新增控件重新把整窗生命周期延长。TitleBarControl 会通过 DependencyPropertyDescriptor 监听父窗口的 WindowStyle / ResizeMode;普通 Window.Close() 不会触发其视觉父级清理。ImageTranslateWindow.OnClosing 会显式解除这两个监听并替换 modern window template,OnClosed 再清空内容、输入绑定和 DataContext。ViewModel 被 DI root scope 钉住(两个图片翻译窗口均已修复):ImageTranslateWindowViewModel 注册为 Transient 且实现 IDisposable。Microsoft.Extensions.DI 会跟踪从 root provider 解析的 Disposable Transient 服务,窗口手动调用 Dispose() 也不会把实例从 root scope 的 _disposables 列表移除;实例要到应用退出、根容器释放时才解除引用。
Ioc.Default.CreateScope() 解析 VM,OnClosed 时 scope.Dispose() 释放 VM,使其脱离 root 跟踪列表。其他窗口的同类修复:OcrWindow、WelcomeSetupWindow、SettingsWindow 的 transient VM 同样会从 root provider 解析,现已统一改用 Ioc.Default.CreateScope() 解析,OnClosed 经 ModernWindowLifecycle.Release(this, _serviceScope.Dispose) 释放。
OcrWindow / WelcomeSetupWindow 的 VM 实现了 IDisposable,关闭时释放 scope 会触发 Dispose(),取消对 OcrService / Settings 等单例的事件订阅。SettingsWindowViewModel 未实现 IDisposable,但其各页面与页面 VM(如 HistoryViewModel)为 Scoped 注册,从 root provider 解析会被 root scope 跟踪、Dispose() 永不触发;改用独立 scope 后关闭窗口即可释放已解析的页面 VM,触发其 Dispose() 取消全局订阅。SettingsWindow.OnClosed 在释放 scope 前还会清空 RootFrame.Content 并解绑导航事件,避免长期页面通过 Frame 反向持有已关闭窗口。译文矢量覆盖(已优化):
Freeze 的 _sourceImage,译文由 ImageTranslateOverlay 按原图坐标执行 retained-mode 矢量绘制。RenderTargetBitmap;该结果位图和对应常驻原生帧缓冲已移除。标注图仍只按原始分辨率生成。回归测试:SnackbarLifecycleTests 覆盖 NoticeBar.IsOpen 可见性、容器重复释放、显示/隐藏动画最终状态,以及 NoticeBar/SnackbarContainer 在重复创建释放后可被 GC 回收;ModernWindowLifecycleTests 验证 modern 窗口关闭后 WindowStyle / ResizeMode 的属性描述符 tracker 已移除,并验证视觉树引用被清空。
诊断方法:复现泄漏后用 dotnet-gcdump report 看是否有 ImageTranslateCompactWindow/ImageTranslateWindowViewModel 实例数随操作次数线性增长;用 dotnet-dump analyze 的 gcroot <地址> 追踪引用链,确认是 PropertyDescriptor._propertyMap 静态链还是 ServiceProviderEngineScope._disposables 钉住。
STranslate/ViewModels/ImageTranslateWindowViewModel.csSTranslate/Views/ImageTranslateWindow.xaml.csSTranslate/Views/ImageTranslateCompactWindow.xamlSTranslate/Controls/NoticeBar.xamlSTranslate/Controls/SnackbarContainer.xamlSTranslate/Controls/ImageTranslateOverlay.csSTranslate/Core/Snackbar.csSTranslate/Core/ImageTranslateOverlayDocument.csSTranslate/Helpers/ImageTranslateRenderer.csSTranslate/Helpers/ModernWindowLifecycle.csSTranslate/Core/Screenshot.csSTranslate/Core/OcrLayoutAnalyzer.csSTranslate/Core/OcrLayoutBlock.csSTranslate/Core/ImageTranslateTextOverlayLayout.csSTranslate/Core/LayoutAnalysisModeJsonConverter.csSTranslate/Services/OcrService.csSTranslate/Services/TranslateService.csSTranslate.Plugin/IOcrPlugin.csTests/STranslate.Tests/OcrLayoutAnalyzerTests.csTests/STranslate.Tests/ImageTranslateTextOverlayLayoutTests.csTests/STranslate.Tests/ImageTranslateOverlayTests.csTests/STranslate.Tests/ModernWindowLifecycleTests.csTests/STranslate.Tests/SnackbarLifecycleTests.csOcrLayoutAnalyzer,并补 OcrLayoutAnalyzerTests。ImageTranslateTextOverlayLayout,并补 ImageTranslateTextOverlayLayoutTests。OcrResult.Regions,并确保每个 OcrContent 有图片像素坐标 BoxPoints。OcrService.IsImageTranslateOcrService() / GetImageTranslateOcrServices()。ImageTranslateWindowViewModel.OnTransFilter() 或 TranslateService.ImageTranslateService 相关逻辑。RefreshSelectableOcrWords()、OcrWordBuilder 或 ImageZoom 的选区逻辑。ImageTranslateCompactWindow 的 PlaceForCapture / PlaceOnPhysicalWindowBounds,选区物理坐标由 Screenshot.GetScreenshotCaptureAsync 经 ScreenGrab CaptureWithRegionAsync 直接回传。ImageTranslateCompactWindowPlacement.CreateLayout,并补 ImageTranslateCompactWindowPlacementTests 对应场景;按钮条尺寸/间距常量在 ImageTranslateCompactWindow(ToolbarWidth/GapH/GapV/WindowMargin)。ImageTranslateCompactWindow.OnClosed → DetachVisualTree + _serviceScope.Dispose;独立窗口关闭走 DetachModernWindowStyle → DetachVisualTree → _serviceScope.Dispose;消息提示生命周期由 Snackbar、SnackbarContainer 和 NoticeBar 负责,禁止重新引入未配对 DependencyPropertyDescriptor.AddValueChanged 的控件。