Files
DCIT/DCIT.Core/Services/DocumentProcessingService.cs

4434 lines
174 KiB
C#
Raw Permalink Normal View History

2026-07-13 10:04:36 +08:00
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using DCIT.Core.Models;
using DCIT.Core.Utilities;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using A = DocumentFormat.OpenXml.Drawing;
using C = DocumentFormat.OpenXml.Drawing.Charts;
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
using M = DocumentFormat.OpenXml.Math;
using Svg;
using V = DocumentFormat.OpenXml.Vml;
using W = DocumentFormat.OpenXml.Wordprocessing;
namespace DCIT.Core.Services
{
public class DocumentProcessingService : IDocumentProcessingService
{
private static readonly string[] SupportedRasterExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tif", ".tiff" };
private static readonly string[] SupportedNonBitmapExtensions = { ".wmf", ".emf", ".svg" };
private static readonly string[] SupportedVisioPackageExtensions = { ".vsd", ".vsdx", ".vsdm" };
private static readonly string[] BrokenFieldResultMarkers =
{
"错误!未找到引用源",
"错误!未找到引用源",
"错误! 未找到引用源",
"错误! 未找到引用源",
"错误!书签未定义",
"错误!书签未定义",
"Error! Reference source not found",
"Error! Bookmark not defined",
};
private const string SupportedDiffVersion = "1.0";
private const string SupportedBmpCodecVersion = "1.0";
private const int WorkingDocumentOpenRetryCount = 5;
private const int WorkingDocumentOpenRetryDelayMilliseconds = 150;
private const double NoiseTargetPixelRatio = 0.55d;
private const int NoiseCellSize = 16;
private const int BrokenFieldResultScanLimit = 50;
private readonly DiffSerializationService _diffSerializationService;
private readonly IDocConversionService _docConversionService;
private readonly IDocumentFieldUpdateService _documentFieldUpdateService;
private readonly IFontValidationService _fontValidationService;
public DocumentProcessingService(DiffSerializationService diffSerializationService)
: this(diffSerializationService, new OfficeDocConversionService(), null, new FontValidationService())
{
}
public DocumentProcessingService(DiffSerializationService diffSerializationService, IDocConversionService docConversionService)
: this(diffSerializationService, docConversionService, docConversionService as IDocumentFieldUpdateService, new FontValidationService())
{
}
public DocumentProcessingService(
DiffSerializationService diffSerializationService,
IDocConversionService docConversionService,
IDocumentFieldUpdateService documentFieldUpdateService)
: this(diffSerializationService, docConversionService, documentFieldUpdateService, new FontValidationService())
{
}
public DocumentProcessingService(
DiffSerializationService diffSerializationService,
IDocConversionService docConversionService,
IDocumentFieldUpdateService documentFieldUpdateService,
IFontValidationService fontValidationService)
{
if (diffSerializationService == null)
{
throw new ArgumentNullException("diffSerializationService");
}
if (fontValidationService == null)
{
throw new ArgumentNullException("fontValidationService");
}
_diffSerializationService = diffSerializationService;
_docConversionService = docConversionService;
_documentFieldUpdateService = documentFieldUpdateService;
_fontValidationService = fontValidationService;
}
public FileProcessResult Replace(ReplaceFileRequest request, IProgress<FileProcessProgress> progress, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
var sourceExtension = NormalizeWordExtension(request.SourcePath);
var outputExtension = NormalizeWordExtension(request.OutputPath);
EnsureDirectoryForFile(request.OutputPath);
EnsureDirectoryForFile(request.DiffPath);
var finalPaths = GetUniqueReplacementOutputPaths(request.OutputPath, request.DiffPath);
var finalOutputPath = finalPaths.OutputPath;
var finalDiffPath = finalPaths.DiffPath;
var finalBmpPath = finalPaths.BmpPath;
var tempOutputPath = CreateTempPath(finalOutputPath);
var tempDiffPath = CreateTempPath(finalDiffPath);
var tempBmpPath = _diffSerializationService.GetBmpPath(tempDiffPath);
var workingDocxPath = CreateWorkingDocxPath("replace");
SafeDelete(tempOutputPath);
SafeDelete(tempDiffPath);
SafeDelete(tempBmpPath);
SafeDelete(workingDocxPath);
try
{
PrepareWorkingDocx(request.SourcePath, workingDocxPath, sourceExtension);
var fontValidation = _fontValidationService.EnsureFontsAvailable(workingDocxPath);
ReportFontFallback(progress, request.SourcePath, fontValidation);
var baselineFieldFindings = ScanBrokenFieldResults(workingDocxPath);
var rules = (request.RuleFile == null ? new List<RuleDefinition>() : request.RuleFile.Rules)
.Where(rule => rule != null && rule.IsEnabled && !string.IsNullOrEmpty(rule.SearchText) && rule.Target == RuleTarget.DocumentContent)
.ToList();
var diff = _diffSerializationService.CreateBaseDocument();
diff.SourceDocument = BuildDocumentInfo(request.SourcePath, request.SourceRootDirectory);
diff.RuleFile = BuildRuleFileInfo(request.RuleFilePath, request.RuleFile, rules);
diff.Payload = new DiffPayload();
var touchedRoots = new HashSet<OpenXmlPartRootElement>();
using (var document = OpenEditableWorkingDocument(request.SourcePath, workingDocxPath, "replace"))
{
cancellationToken.ThrowIfCancellationRequested();
ProcessTextReplacements(document, rules, diff.Payload, touchedRoots, progress, request.SourcePath, cancellationToken);
if (request.EnableImageReplacement)
{
ProcessImageReplacements(document, diff.Payload, request.FixedImageSeed, cancellationToken);
}
SaveTouchedRoots(touchedRoots, document);
}
ValidateNoBrokenFieldResults(workingDocxPath, request.SourcePath, baselineFieldFindings, "替换", progress);
CommitWorkingDocument(workingDocxPath, tempOutputPath, outputExtension);
diff.ReplacedDocument = BuildDocumentInfo(tempOutputPath, Path.GetDirectoryName(finalOutputPath) ?? string.Empty, finalOutputPath);
RecordFileNameChangeOperation(diff);
diff.Payload.Statistics.TextReplaceCount = diff.Payload.Operations.Count(item => string.Equals(item.Type, "text", StringComparison.Ordinal));
diff.Payload.Statistics.ImageReplaceCount = diff.Payload.Operations.Count(item => string.Equals(item.Type, "image", StringComparison.Ordinal));
_diffSerializationService.Write(diff, tempDiffPath, request.EncryptDiff);
File.Move(tempOutputPath, finalOutputPath);
File.Move(tempDiffPath, finalDiffPath);
File.Move(tempBmpPath, finalBmpPath);
return new FileProcessResult
{
OutputPath = finalOutputPath,
DiffPath = finalDiffPath,
BmpPath = finalBmpPath,
TextOperationCount = diff.Payload.Statistics.TextReplaceCount,
ImageOperationCount = diff.Payload.Statistics.ImageReplaceCount,
};
}
catch
{
SafeDelete(tempOutputPath);
SafeDelete(tempDiffPath);
SafeDelete(tempBmpPath);
throw;
}
finally
{
SafeDelete(workingDocxPath);
}
}
public FileProcessResult Restore(RestoreFileRequest request, IProgress<FileProcessProgress> progress, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
var replacedExtension = NormalizeWordExtension(request.ReplacedPath);
var outputExtension = NormalizeWordExtension(request.OutputPath);
var loaded = _diffSerializationService.Load(request.DiffPath);
if (loaded.Document == null || loaded.Document.Payload == null)
{
throw new InvalidDataException("差异文件缺少有效载荷。");
}
var currentFingerprint = HashUtility.ComputeFileSha256Base64(request.ReplacedPath);
if (!string.Equals(currentFingerprint, loaded.Document.ReplacedDocument.Fingerprint, StringComparison.Ordinal))
{
throw new InvalidDataException(BuildReplacedFingerprintMismatchMessage(request.ReplacedPath, loaded.Document, currentFingerprint));
}
ValidateRestoreCompatibility(request, loaded.Document);
ValidateDiffAlgorithm(loaded.Document);
EnsureDirectoryForFile(request.OutputPath);
var finalOutputPath = GetUniquePath(request.OutputPath);
var tempOutputPath = CreateTempPath(finalOutputPath);
var workingDocxPath = CreateWorkingDocxPath("restore");
SafeDelete(tempOutputPath);
SafeDelete(workingDocxPath);
try
{
PrepareWorkingDocx(request.ReplacedPath, workingDocxPath, replacedExtension);
var fontValidation = _fontValidationService.EnsureFontsAvailable(workingDocxPath);
ReportFontFallback(progress, request.ReplacedPath, fontValidation);
var baselineFieldFindings = ScanBrokenFieldResults(workingDocxPath);
var touchedRoots = new HashSet<OpenXmlPartRootElement>();
using (var document = OpenEditableWorkingDocument(request.ReplacedPath, workingDocxPath, "restore"))
{
cancellationToken.ThrowIfCancellationRequested();
RestoreText(document, loaded.Document.Payload, progress, request.ReplacedPath, cancellationToken, touchedRoots);
RestoreImages(document, loaded.Document.Payload, cancellationToken);
SaveTouchedRoots(touchedRoots, document);
}
ValidateNoBrokenFieldResults(workingDocxPath, request.ReplacedPath, baselineFieldFindings, "恢复", progress);
CommitWorkingDocument(workingDocxPath, tempOutputPath, outputExtension);
File.Move(tempOutputPath, finalOutputPath);
return new FileProcessResult
{
OutputPath = finalOutputPath,
DiffPath = request.DiffPath,
BmpPath = Path.GetExtension(request.DiffPath).Equals(".bmp", StringComparison.OrdinalIgnoreCase) ? request.DiffPath : _diffSerializationService.GetBmpPath(request.DiffPath),
TextOperationCount = loaded.Document.Payload.Statistics.TextReplaceCount,
ImageOperationCount = loaded.Document.Payload.Statistics.ImageReplaceCount,
};
}
catch
{
SafeDelete(tempOutputPath);
throw;
}
finally
{
SafeDelete(workingDocxPath);
}
}
private void ValidateRestoreCompatibility(RestoreFileRequest request, DiffDocument diff)
{
if (diff == null)
{
throw new ArgumentNullException("diff");
}
if (diff.App == null
|| !string.Equals(diff.App.Name, "DCIT", StringComparison.Ordinal)
|| !string.Equals(diff.App.Version, _diffSerializationService.AppVersion, StringComparison.Ordinal))
{
throw new InvalidDataException("差异文件程序版本与当前程序不兼容。");
}
if (diff.RuleFile != null
&& !string.IsNullOrWhiteSpace(diff.RuleFile.Fingerprint)
&& DiffContainsTextOperations(diff))
{
if (string.IsNullOrWhiteSpace(request.CurrentRuleFilePath) || !File.Exists(request.CurrentRuleFilePath))
{
throw new InvalidDataException("恢复时必须提供与差异文件匹配的规则文件。");
}
var currentRuleFingerprint = HashUtility.ComputeFileSha256Base64(request.CurrentRuleFilePath);
if (!string.Equals(currentRuleFingerprint, diff.RuleFile.Fingerprint, StringComparison.Ordinal))
{
throw new InvalidDataException("当前规则文件与差异文件记录的规则指纹不匹配。");
}
}
}
private static void RecordFileNameChangeOperation(DiffDocument diff)
{
if (diff == null || diff.Payload == null)
{
return;
}
if (diff.Payload.Operations == null)
{
diff.Payload.Operations = new List<DiffOperation>();
}
var originalFileName = diff.SourceDocument == null ? null : diff.SourceDocument.FileName;
var replacedFileName = diff.ReplacedDocument == null ? null : diff.ReplacedDocument.FileName;
if (string.IsNullOrWhiteSpace(originalFileName)
|| string.IsNullOrWhiteSpace(replacedFileName)
|| string.Equals(originalFileName, replacedFileName, StringComparison.Ordinal))
{
return;
}
diff.Payload.Operations.Insert(0, new DiffOperation
{
OperationId = "F000001",
Type = "fileName",
ObjectType = "documentFileName",
Position = new DiffPosition
{
StoryType = "file",
ContainerPath = "fileName",
ParagraphIndex = -1,
},
OriginalText = originalFileName,
WrittenText = replacedFileName,
});
}
private static bool DiffContainsTextOperations(DiffDocument diff)
{
return diff != null
&& diff.Payload != null
&& diff.Payload.Operations != null
&& diff.Payload.Operations.Any(item => item != null && string.Equals(item.Type, "text", StringComparison.Ordinal));
}
private static string BuildReplacedFingerprintMismatchMessage(string replacedPath, DiffDocument diff, string currentFingerprint)
{
var replaced = diff == null ? null : diff.ReplacedDocument;
return string.Format(
CultureInfo.InvariantCulture,
"恢复输入文档与差异文件记录的替换后文档指纹不匹配。 当前文件={0}; 差异记录文件名={1}; 差异记录相对路径={2}; 指纹算法={3}; 当前指纹={4}; 期望指纹={5}",
replacedPath ?? string.Empty,
replaced == null ? string.Empty : replaced.FileName ?? string.Empty,
replaced == null ? string.Empty : replaced.RelativePath ?? string.Empty,
replaced == null ? "SHA-256" : replaced.FingerprintAlgorithm ?? "SHA-256",
currentFingerprint ?? string.Empty,
replaced == null ? string.Empty : replaced.Fingerprint ?? string.Empty);
}
private static void ValidateDiffAlgorithm(DiffDocument diff)
{
if (diff.Algorithm == null
|| !string.Equals(diff.Algorithm.DiffVersion, SupportedDiffVersion, StringComparison.Ordinal)
|| !string.Equals(diff.Algorithm.BmpCodecVersion, SupportedBmpCodecVersion, StringComparison.Ordinal))
{
throw new InvalidDataException("差异文件算法版本不受支持。");
}
}
private void PrepareWorkingDocx(string sourcePath, string workingDocxPath, string sourceExtension)
{
EnsureDocConversionService(sourceExtension);
EnsureDirectoryForFile(workingDocxPath);
if (string.Equals(sourceExtension, ".docx", StringComparison.OrdinalIgnoreCase))
{
File.Copy(sourcePath, workingDocxPath, true);
}
else
{
_docConversionService.Convert(sourcePath, workingDocxPath);
}
EnsureWritableFile(workingDocxPath);
VerifyWorkingDocxIntegrity(workingDocxPath, sourcePath, sourceExtension);
}
private static void VerifyWorkingDocxIntegrity(string workingDocxPath, string sourcePath, string sourceExtension)
{
try
{
using (var document = WordprocessingDocument.Open(workingDocxPath, false))
{
if (document.MainDocumentPart == null)
{
throw new InvalidDataException("工作文档缺少 MainDocumentPart。");
}
}
}
catch (Exception ex)
{
var fileInfo = new FileInfo(workingDocxPath);
throw new InvalidDataException(
string.Format(
CultureInfo.InvariantCulture,
"准备工作文档失败:生成的 .docx 不是有效的 ZIP/OpenXML 文件。源文件={0}; 工作文档={1}; 源扩展名={2}; 工作文件大小={3} 字节; 内部异常={4}: {5}",
sourcePath ?? string.Empty,
workingDocxPath ?? string.Empty,
sourceExtension ?? string.Empty,
fileInfo.Exists ? fileInfo.Length : -1,
ex == null ? string.Empty : ex.GetType().Name,
ex == null ? string.Empty : ex.Message),
ex);
}
}
private static WordprocessingDocument OpenEditableWorkingDocument(string sourcePath, string workingDocxPath, string stage)
{
Exception lastError = null;
for (var attempt = 1; attempt <= WorkingDocumentOpenRetryCount; attempt++)
{
try
{
EnsureWritableFile(workingDocxPath);
return WordprocessingDocument.Open(workingDocxPath, true);
}
catch (UnauthorizedAccessException ex)
{
lastError = ex;
}
catch (IOException ex)
{
lastError = ex;
}
if (attempt < WorkingDocumentOpenRetryCount)
{
Thread.Sleep(WorkingDocumentOpenRetryDelayMilliseconds * attempt);
}
}
throw new IOException(BuildWorkingDocumentOpenFailureMessage(sourcePath, workingDocxPath, stage, lastError), lastError);
}
private void CommitWorkingDocument(string workingDocxPath, string targetPath, string targetExtension)
{
EnsureDocConversionService(targetExtension);
EnsureDirectoryForFile(targetPath);
if (string.Equals(targetExtension, ".docx", StringComparison.OrdinalIgnoreCase))
{
File.Copy(workingDocxPath, targetPath, true);
return;
}
_docConversionService.Convert(workingDocxPath, targetPath);
}
private void EnsureDocConversionService(string extension)
{
if (string.Equals(extension, ".doc", StringComparison.OrdinalIgnoreCase) && _docConversionService == null)
{
throw new NotSupportedException("当前配置未提供 .doc 转换能力。");
}
}
private static void EnsureDirectoryForFile(string path)
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
}
private void UpdateDynamicContent(string workingDocxPath)
{
if (_documentFieldUpdateService == null || string.IsNullOrWhiteSpace(workingDocxPath))
{
return;
}
_documentFieldUpdateService.UpdateDynamicContent(workingDocxPath);
}
private static void ReportFontFallback(IProgress<FileProcessProgress> progress, string filePath, FontValidationResult result)
{
if (progress == null || result == null || result.MissingFonts == null || result.MissingFonts.Count == 0)
{
return;
}
progress.Report(new FileProcessProgress
{
FilePath = filePath,
FilePercent = 0,
Message = "检测到缺失字体: " + string.Join(", ", result.MissingFonts) + "。已使用默认字体继续处理:中文/东亚=宋体,英文和数字=Times New Roman。",
IsWarning = true,
});
}
private static List<BrokenFieldFinding> ScanBrokenFieldResults(string workingDocxPath)
{
var findings = new List<BrokenFieldFinding>();
if (string.IsNullOrWhiteSpace(workingDocxPath) || !File.Exists(workingDocxPath))
{
return findings;
}
using (var document = WordprocessingDocument.Open(workingDocxPath, false))
{
foreach (var context in EnumerateTextParts(document))
{
foreach (var target in EnumerateTextTargets(context))
{
var state = BuildParagraphState(target);
if (string.IsNullOrEmpty(state.Text))
{
continue;
}
foreach (var marker in BrokenFieldResultMarkers)
{
var searchStart = 0;
while (searchStart < state.Text.Length)
{
var index = state.Text.IndexOf(marker, searchStart, StringComparison.OrdinalIgnoreCase);
if (index < 0)
{
break;
}
findings.Add(BuildBrokenFieldFinding(target, state.Text, marker, index));
searchStart = index + Math.Max(marker.Length, 1);
if (findings.Count >= BrokenFieldResultScanLimit)
{
break;
}
}
if (findings.Count >= BrokenFieldResultScanLimit)
{
break;
}
}
if (findings.Count >= BrokenFieldResultScanLimit)
{
break;
}
}
if (findings.Count >= BrokenFieldResultScanLimit)
{
break;
}
}
}
return findings;
}
private static BrokenFieldFinding BuildBrokenFieldFinding(ParagraphTarget target, string paragraphText, string marker, int index)
{
var paragraphElement = target == null ? null : target.ParagraphElement;
var hitCell = FindHitTableCell(paragraphElement);
TryParseTableLocation(target == null ? null : target.ContainerPath, out var tableIndex, out var rowIndex, out var columnIndex);
var fullText = NormalizeWhitespaceForReport(paragraphText ?? string.Empty);
var previousText = GetSiblingParagraphText(paragraphElement, true);
var nextText = GetSiblingParagraphText(paragraphElement, false);
var sameRowCells = GetSameRowCellsSnapshot(paragraphElement, hitCell);
var sameColumnCells = GetSameColumnCellsSnapshot(paragraphElement, hitCell);
return new BrokenFieldFinding
{
StoryType = target == null ? string.Empty : (target.StoryType ?? string.Empty),
ContainerPath = target == null ? string.Empty : (target.ContainerPath ?? string.Empty),
ParagraphIndex = target == null ? 0 : target.ParagraphIndex,
Marker = marker,
Excerpt = CreateValidationExcerpt(paragraphText, index, marker.Length),
FullParagraphText = TruncateForReport(fullText, 500),
TableIndex = tableIndex,
RowIndex = rowIndex,
ColumnIndex = columnIndex,
PreviousParagraphText = previousText,
NextParagraphText = nextText,
SameRowCellsText = sameRowCells,
SameColumnCellsText = sameColumnCells,
};
}
private static void ValidateNoBrokenFieldResults(
string workingDocxPath,
string sourcePath,
IReadOnlyCollection<BrokenFieldFinding> baselineFindings,
string operationName,
IProgress<FileProcessProgress> progress)
{
if (string.IsNullOrWhiteSpace(workingDocxPath) || !File.Exists(workingDocxPath))
{
return;
}
var currentFindings = ScanBrokenFieldResults(workingDocxPath);
if (currentFindings.Count == 0)
{
return;
}
var baselineIdentities = new HashSet<string>(
(baselineFindings ?? Enumerable.Empty<BrokenFieldFinding>()).Select(item => item.Identity),
StringComparer.Ordinal);
var newFindings = currentFindings
.Where(item => !baselineIdentities.Contains(item.Identity))
.ToList();
var baselineCount = currentFindings.Count - newFindings.Count;
if (baselineCount > 0)
{
progress?.Report(new FileProcessProgress
{
FilePath = sourcePath ?? string.Empty,
FilePercent = 0,
Message = string.Format(
CultureInfo.InvariantCulture,
"检测到源文档已存在 {0} 处交叉引用/域结果错误标记(属基线已存在问题),将继续完成{1}。",
baselineCount,
operationName ?? string.Empty),
IsWarning = true,
});
}
if (newFindings.Count == 0)
{
return;
}
var detailBuilder = new StringBuilder();
for (var i = 0; i < newFindings.Count; i++)
{
detailBuilder.AppendLine();
detailBuilder.AppendLine("=== 命中 [" + (i + 1).ToString(CultureInfo.InvariantCulture) + "] ===");
AppendFindingDetail(detailBuilder, newFindings[i]);
}
throw new InvalidDataException(
string.Format(
CultureInfo.InvariantCulture,
"文档验证失败:检测到{0}过程中新增的交叉引用/域结果错误,禁止输出当前{1}结果。源文件={2}; 工作文件={3}; 新增命中数={4}; 基线已命中数={5}; 明细如下:{6}",
operationName ?? string.Empty,
operationName ?? string.Empty,
sourcePath ?? string.Empty,
workingDocxPath,
newFindings.Count,
baselineCount,
detailBuilder));
}
private static void AppendFindingDetail(StringBuilder sb, BrokenFieldFinding finding)
{
if (finding == null)
{
return;
}
sb.AppendLine(" Story = " + (finding.StoryType ?? string.Empty));
sb.AppendLine(" Container = " + (finding.ContainerPath ?? string.Empty));
if (finding.TableIndex.HasValue && finding.RowIndex.HasValue && finding.ColumnIndex.HasValue)
{
sb.AppendLine(string.Format(
CultureInfo.InvariantCulture,
" 表格位置 = 第{0}个表 第{1}行 第{2}列 (基于0的索引)",
finding.TableIndex.Value,
finding.RowIndex.Value,
finding.ColumnIndex.Value));
}
sb.AppendLine(" Paragraph = " + finding.ParagraphIndex.ToString(CultureInfo.InvariantCulture));
sb.AppendLine(" Marker = " + (finding.Marker ?? string.Empty));
sb.AppendLine(" Excerpt = " + (finding.Excerpt ?? string.Empty));
sb.AppendLine(" 段落完整文本= " + (finding.FullParagraphText ?? string.Empty));
if (!string.IsNullOrEmpty(finding.PreviousParagraphText))
{
sb.AppendLine(" 前一段落 = " + finding.PreviousParagraphText);
}
if (!string.IsNullOrEmpty(finding.NextParagraphText))
{
sb.AppendLine(" 后一段落 = " + finding.NextParagraphText);
}
if (!string.IsNullOrEmpty(finding.SameRowCellsText))
{
sb.AppendLine(" 同行所有单元格 = " + finding.SameRowCellsText);
}
if (!string.IsNullOrEmpty(finding.SameColumnCellsText))
{
sb.AppendLine(" 同列所有单元格 = " + finding.SameColumnCellsText);
}
}
private static string CreateValidationExcerpt(string text, int index, int length)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
const int Radius = 80;
var start = Math.Max(0, index - Radius);
var end = Math.Min(text.Length, index + length + Radius);
var excerpt = text.Substring(start, end - start)
.Replace('\r', ' ')
.Replace('\n', ' ')
.Replace('\t', ' ');
if (start > 0)
{
excerpt = "..." + excerpt;
}
if (end < text.Length)
{
excerpt += "...";
}
return excerpt;
}
private static string TruncateForReport(string text, int maxLength)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
const string ellipsis = "...";
if (text.Length <= maxLength)
{
return text;
}
if (maxLength <= ellipsis.Length)
{
return text.Substring(0, maxLength);
}
return text.Substring(0, maxLength - ellipsis.Length) + ellipsis;
}
private static string NormalizeWhitespaceForReport(string text)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
var sb = new StringBuilder(text.Length);
var lastWasWhitespace = false;
foreach (var ch in text)
{
if (ch == '\r' || ch == '\n' || ch == '\t' || ch == ' ')
{
if (!lastWasWhitespace)
{
sb.Append(' ');
lastWasWhitespace = true;
}
continue;
}
sb.Append(ch);
lastWasWhitespace = false;
}
return sb.ToString().Trim();
}
private static string ExtractParagraphVisibleText(OpenXmlElement paragraph)
{
if (paragraph == null)
{
return string.Empty;
}
var sb = new StringBuilder();
foreach (var text in paragraph.Descendants<W.Text>())
{
if (text != null)
{
sb.Append(text.Text);
}
}
return sb.ToString();
}
private static void TryParseTableLocation(string containerPath, out int? tableIndex, out int? rowIndex, out int? columnIndex)
{
tableIndex = null;
rowIndex = null;
columnIndex = null;
if (string.IsNullOrWhiteSpace(containerPath))
{
return;
}
var match = Regex.Match(containerPath, @"tbl\[(\d+)\]/tr\[(\d+)\]/tc\[(\d+)\]", RegexOptions.CultureInvariant);
if (!match.Success)
{
return;
}
if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var t))
{
tableIndex = t;
}
if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r))
{
rowIndex = r;
}
if (int.TryParse(match.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var c))
{
columnIndex = c;
}
}
private static string GetSiblingParagraphText(OpenXmlElement paragraph, bool previous)
{
if (paragraph == null)
{
return null;
}
var sibling = previous ? paragraph.PreviousSibling() : paragraph.NextSibling();
if (sibling is W.Paragraph wp)
{
var text = NormalizeWhitespaceForReport(ExtractParagraphVisibleText(wp));
return TruncateForReport(text, 200);
}
return null;
}
private static string GetSameRowCellsSnapshot(OpenXmlElement paragraph, OpenXmlElement hitCell)
{
if (paragraph == null || hitCell == null)
{
return null;
}
var row = hitCell.Parent as W.TableRow;
if (row == null)
{
return null;
}
var sb = new StringBuilder();
var colIdx = 0;
foreach (var cell in row.Elements<W.TableCell>())
{
if (sb.Length > 0)
{
sb.Append(" | ");
}
var firstPara = cell.Descendants<W.Paragraph>().FirstOrDefault();
var text = firstPara != null ? ExtractParagraphVisibleText(firstPara) : string.Empty;
text = NormalizeWhitespaceForReport(text);
var marker = ReferenceEquals(cell, hitCell) ? " <==命中此单元格" : string.Empty;
sb.AppendFormat(
CultureInfo.InvariantCulture,
"[列{0}] {1}{2}",
colIdx,
TruncateForReport(text, 100),
marker);
colIdx++;
}
return sb.ToString();
}
private static string GetSameColumnCellsSnapshot(OpenXmlElement paragraph, OpenXmlElement hitCell)
{
if (paragraph == null || hitCell == null)
{
return null;
}
var row = hitCell.Parent as W.TableRow;
if (row == null)
{
return null;
}
var table = row.Parent as W.Table;
if (table == null)
{
return null;
}
int hitColumnIndex;
int hitRowIndex;
{
var cells = row.Elements<W.TableCell>().ToList();
hitColumnIndex = cells.TakeWhile(c => !ReferenceEquals(c, hitCell)).Count();
var rows = table.Elements<W.TableRow>().ToList();
hitRowIndex = rows.TakeWhile(r => !ReferenceEquals(r, row)).Count();
}
var sb = new StringBuilder();
var rowIdx = 0;
foreach (var currentRow in table.Elements<W.TableRow>())
{
var cells = currentRow.Elements<W.TableCell>().ToList();
if (hitColumnIndex < cells.Count)
{
if (sb.Length > 0)
{
sb.Append(" | ");
}
var firstPara = cells[hitColumnIndex].Descendants<W.Paragraph>().FirstOrDefault();
var text = firstPara != null ? ExtractParagraphVisibleText(firstPara) : string.Empty;
text = NormalizeWhitespaceForReport(text);
var marker = rowIdx == hitRowIndex ? " <==命中此行" : string.Empty;
sb.AppendFormat(
CultureInfo.InvariantCulture,
"[行{0}] {1}{2}",
rowIdx,
TruncateForReport(text, 100),
marker);
}
rowIdx++;
}
return sb.ToString();
}
private static OpenXmlElement FindHitTableCell(OpenXmlElement paragraph)
{
if (paragraph == null)
{
return null;
}
foreach (var ancestor in paragraph.Ancestors())
{
if (ancestor is W.TableCell)
{
return ancestor;
}
}
return null;
}
private static string NormalizeWordExtension(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
if (!extension.Equals(".doc", StringComparison.OrdinalIgnoreCase)
&& !extension.Equals(".docx", StringComparison.OrdinalIgnoreCase))
{
throw new NotSupportedException("当前版本仅支持 .doc 与 .docx 文档。");
}
return extension;
}
private static DiffDocumentInfo BuildDocumentInfo(string filePath, string rootDirectory)
{
return BuildDocumentInfo(filePath, rootDirectory, filePath);
}
private static DiffDocumentInfo BuildDocumentInfo(string fingerprintSourcePath, string rootDirectory, string logicalPath)
{
var metadataPath = string.IsNullOrWhiteSpace(logicalPath) ? fingerprintSourcePath : logicalPath;
var relativePath = metadataPath;
if (!string.IsNullOrWhiteSpace(rootDirectory) && Directory.Exists(rootDirectory))
{
try
{
relativePath = PathUtility.GetRelativePath(rootDirectory, metadataPath);
}
catch
{
relativePath = Path.GetFileName(metadataPath);
}
}
return new DiffDocumentInfo
{
FileName = Path.GetFileName(metadataPath),
RelativePath = relativePath,
Extension = Path.GetExtension(metadataPath),
FingerprintAlgorithm = "SHA-256",
Fingerprint = HashUtility.ComputeFileSha256Base64(fingerprintSourcePath),
};
}
private static DiffRuleFileInfo BuildRuleFileInfo(string ruleFilePath, RuleFile ruleFile, IList<RuleDefinition> effectiveRules)
{
var fileName = string.IsNullOrWhiteSpace(ruleFilePath) ? "unsaved.rule" : Path.GetFileName(ruleFilePath);
string fingerprint;
if (!string.IsNullOrWhiteSpace(ruleFilePath) && File.Exists(ruleFilePath))
{
fingerprint = HashUtility.ComputeFileSha256Base64(ruleFilePath);
}
else if (effectiveRules == null || effectiveRules.Count == 0)
{
fileName = string.Empty;
fingerprint = string.Empty;
}
else
{
var json = Newtonsoft.Json.JsonConvert.SerializeObject(ruleFile ?? new RuleFile(), Newtonsoft.Json.Formatting.None);
fingerprint = HashUtility.ComputeSha256Base64(json);
}
return new DiffRuleFileInfo
{
FileName = fileName,
Version = ruleFile == null ? "1.0" : ruleFile.Version ?? "1.0",
FingerprintAlgorithm = "SHA-256",
Fingerprint = fingerprint,
};
}
private static string CreateTempPath(string finalPath)
{
var directory = Path.GetDirectoryName(finalPath) ?? string.Empty;
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var name = Path.GetFileNameWithoutExtension(finalPath);
var extension = Path.GetExtension(finalPath);
return Path.Combine(directory, string.Format("{0}.tmp-{1}{2}", name, Guid.NewGuid().ToString("N"), extension));
}
private static string CreateWorkingDocxPath(string prefix)
{
var directory = Path.Combine(Path.GetTempPath(), "DCIT", "processing", prefix ?? "document");
Directory.CreateDirectory(directory);
return Path.Combine(directory, Guid.NewGuid().ToString("N") + ".docx");
}
private static void EnsureWritableFile(string path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return;
}
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.ReadOnly) != 0)
{
File.SetAttributes(path, attributes & ~FileAttributes.ReadOnly);
}
}
private static string GetUniquePath(string requestedPath)
{
if (!File.Exists(requestedPath))
{
return requestedPath;
}
var directory = Path.GetDirectoryName(requestedPath) ?? string.Empty;
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(requestedPath);
var extension = Path.GetExtension(requestedPath);
var index = 1;
string candidate;
do
{
candidate = Path.Combine(directory, string.Format("{0}_{1}{2}", fileNameWithoutExtension, index, extension));
index++;
}
while (File.Exists(candidate));
return candidate;
}
private ReplacementOutputPaths GetUniqueReplacementOutputPaths(string requestedOutputPath, string requestedDiffPath)
{
var outputDirectory = Path.GetDirectoryName(requestedOutputPath) ?? string.Empty;
var diffDirectory = Path.GetDirectoryName(requestedDiffPath) ?? string.Empty;
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(requestedOutputPath);
var diffFileNameWithoutExtension = Path.GetFileNameWithoutExtension(requestedDiffPath);
var outputExtension = Path.GetExtension(requestedOutputPath);
var diffExtension = Path.GetExtension(requestedDiffPath);
var index = 0;
while (true)
{
var suffix = index == 0
? string.Empty
: "_" + index.ToString(CultureInfo.InvariantCulture);
var outputPath = Path.Combine(outputDirectory, outputFileNameWithoutExtension + suffix + outputExtension);
var diffPath = Path.Combine(diffDirectory, diffFileNameWithoutExtension + suffix + diffExtension);
var bmpPath = _diffSerializationService.GetBmpPath(diffPath);
if (!File.Exists(outputPath) && !File.Exists(diffPath) && !File.Exists(bmpPath))
{
return new ReplacementOutputPaths
{
OutputPath = outputPath,
DiffPath = diffPath,
BmpPath = bmpPath,
};
}
index++;
}
}
private static void SafeDelete(string path)
{
try
{
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
File.Delete(path);
}
}
catch
{
}
}
private static string BuildWorkingDocumentOpenFailureMessage(string sourcePath, string workingDocxPath, string stage, Exception exception)
{
var builder = new StringBuilder();
builder.Append("无法以可编辑方式打开临时工作副本。");
builder.Append(" 阶段=").Append(stage ?? string.Empty);
builder.Append("; 源文件=").Append(sourcePath ?? string.Empty);
builder.Append("; 临时工作文件=").Append(workingDocxPath ?? string.Empty);
builder.Append("; 尝试次数=").Append(WorkingDocumentOpenRetryCount.ToString(CultureInfo.InvariantCulture));
AppendFileSnapshot(builder, "源文件", sourcePath);
AppendFileSnapshot(builder, "临时工作文件", workingDocxPath);
if (exception != null)
{
builder.Append("; 最近异常=").Append(exception.GetType().FullName).Append(": ").Append(exception.Message);
}
return builder.ToString();
}
private static void AppendFileSnapshot(StringBuilder builder, string label, string path)
{
if (builder == null || string.IsNullOrWhiteSpace(path))
{
return;
}
try
{
if (!File.Exists(path))
{
builder.Append("; ").Append(label).Append("存在=否");
return;
}
var info = new FileInfo(path);
builder.Append("; ").Append(label).Append("存在=是");
builder.Append("; ").Append(label).Append("大小=").Append(info.Length.ToString(CultureInfo.InvariantCulture));
builder.Append("; ").Append(label).Append("属性=").Append(info.Attributes.ToString());
builder.Append("; ").Append(label).Append("只读=").Append(info.IsReadOnly ? "是" : "否");
builder.Append("; ").Append(label).Append("最后写入时间=").Append(info.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture));
builder.Append("; ").Append(label).Append("最后写入时间(UTC)=").Append(info.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture));
builder.Append("; ").Append(label).Append("SHA256=").Append(HashUtility.ComputeFileSha256Base64(path));
}
catch (Exception ex)
{
builder.Append("; ").Append(label).Append("快照失败=").Append(ex.GetType().FullName).Append(": ").Append(ex.Message);
}
}
private static IEnumerable<PartContext> EnumerateTextParts(WordprocessingDocument document)
{
var main = document.MainDocumentPart;
if (main == null)
{
yield break;
}
if (main.Document != null)
{
yield return new PartContext
{
Part = main,
Root = main.Document,
PartUri = main.Uri.ToString(),
StoryType = "main",
};
}
var queue = new Queue<OpenXmlPartContainer>();
var visited = new HashSet<OpenXmlPart>();
queue.Enqueue(main);
while (queue.Count > 0)
{
var container = queue.Dequeue();
foreach (var pair in container.Parts)
{
var part = pair.OpenXmlPart;
if (part == null || !visited.Add(part))
{
continue;
}
if (part.RootElement != null && IsTextRelevantPart(part))
{
yield return new PartContext
{
Part = part,
Root = part.RootElement,
PartUri = part.Uri.ToString(),
StoryType = GetStoryType(part),
};
}
queue.Enqueue(part);
}
}
}
private static IEnumerable<ImageContext> EnumerateImageParts(WordprocessingDocument document)
{
foreach (var partContext in EnumerateTextParts(document))
{
var drawings = partContext.Root.Descendants<W.Drawing>().ToList();
for (var index = 0; index < drawings.Count; index++)
{
var drawing = drawings[index];
var blip = drawing.Descendants<A.Blip>()
.FirstOrDefault(item => item.Embed != null && !string.IsNullOrWhiteSpace(item.Embed.Value));
if (blip == null)
{
continue;
}
ImagePart imagePart;
try
{
imagePart = partContext.Part.GetPartById(blip.Embed.Value) as ImagePart;
}
catch
{
imagePart = null;
}
if (imagePart == null)
{
continue;
}
yield return CreateImageContext(partContext, drawing, blip, imagePart, index);
}
var vmlContainers = partContext.Root
.Descendants()
.Where(item =>
(item is W.Picture picture && !picture.Ancestors<W.EmbeddedObject>().Any())
|| item is W.EmbeddedObject)
.ToList();
for (var index = 0; index < vmlContainers.Count; index++)
{
var containerElement = vmlContainers[index];
var imageData = containerElement.Descendants<V.ImageData>()
.FirstOrDefault(item => !string.IsNullOrWhiteSpace(GetVmlRelationshipId(item)));
if (imageData == null)
{
continue;
}
var relationshipId = GetVmlRelationshipId(imageData);
ImagePart imagePart;
try
{
imagePart = partContext.Part.GetPartById(relationshipId) as ImagePart;
}
catch
{
imagePart = null;
}
if (imagePart == null)
{
continue;
}
yield return CreateVmlImageContext(partContext, containerElement, imageData, imagePart, relationshipId, index);
}
}
}
private static ImageContext CreateImageContext(PartContext partContext, W.Drawing drawing, A.Blip blip, ImagePart imagePart, int drawingIndex)
{
var inline = drawing.GetFirstChild<DW.Inline>();
var anchor = drawing.GetFirstChild<DW.Anchor>();
var storyType = drawing.Ancestors().Any(item => string.Equals(item.LocalName, "txbxContent", StringComparison.OrdinalIgnoreCase))
? "textbox"
: partContext.StoryType;
long width = 0;
long height = 0;
if (inline != null && inline.Extent != null)
{
width = inline.Extent.Cx == null ? 0L : inline.Extent.Cx.Value;
height = inline.Extent.Cy == null ? 0L : inline.Extent.Cy.Value;
}
else if (anchor != null && anchor.Extent != null)
{
width = anchor.Extent.Cx == null ? 0L : anchor.Extent.Cx.Value;
height = anchor.Extent.Cy == null ? 0L : anchor.Extent.Cy.Value;
}
var transform = drawing.Descendants<A.Transform2D>().FirstOrDefault();
var rotation = transform != null && transform.Rotation != null
? transform.Rotation.Value / 60000d
: 0d;
return new ImageContext
{
ParentPart = partContext.Part,
ImagePart = imagePart,
RelationshipId = blip.Embed.Value,
RelationKey = (partContext.PartUri ?? string.Empty) + "::" + blip.Embed.Value,
PartUri = imagePart.Uri.ToString(),
StoryType = storyType,
ContainerPath = BuildContainerPath(partContext.PartUri, drawing, partContext.Root),
ContainerKind = "drawing",
ObjectType = inline != null ? "inlineImage" : "floatingImage",
Layout = new DiffImageLayout
{
DisplayWidthEmu = width,
DisplayHeightEmu = height,
WrapMode = DetermineWrapMode(inline, anchor),
RotationDegree = rotation,
},
ObjectIndex = drawingIndex,
};
}
private static ImageContext CreateVmlImageContext(PartContext partContext, OpenXmlElement containerElement, V.ImageData imageData, ImagePart imagePart, string relationshipId, int pictureIndex)
{
var shape = imageData.Ancestors<V.Shape>().FirstOrDefault();
var styleMap = ParseVmlStyle(shape == null ? null : shape.Style == null ? null : shape.Style.Value);
var storyType = containerElement.Ancestors().Any(item => string.Equals(item.LocalName, "txbxContent", StringComparison.OrdinalIgnoreCase))
? "textbox"
: partContext.StoryType;
var embeddedProgramId = GetEmbeddedProgramId(containerElement);
var embeddedPackageExtension = GetEmbeddedPackageExtension(partContext.Part, containerElement);
return new ImageContext
{
ParentPart = partContext.Part,
ImagePart = imagePart,
RelationshipId = relationshipId,
RelationKey = (partContext.PartUri ?? string.Empty) + "::" + relationshipId,
PartUri = imagePart.Uri.ToString(),
StoryType = storyType,
ContainerPath = BuildContainerPath(partContext.PartUri, containerElement, partContext.Root),
ContainerKind = GetContainerKind(containerElement),
EmbeddedProgramId = embeddedProgramId,
EmbeddedPackageExtension = embeddedPackageExtension,
ObjectType = DetermineVmlObjectType(styleMap, containerElement),
Layout = new DiffImageLayout
{
DisplayWidthEmu = ParseVmlDimensionToEmu(styleMap, "width"),
DisplayHeightEmu = ParseVmlDimensionToEmu(styleMap, "height"),
WrapMode = DetermineVmlWrapMode(styleMap, containerElement),
RotationDegree = ParseVmlRotation(styleMap),
},
ObjectIndex = pictureIndex,
};
}
private static string DetermineWrapMode(DW.Inline inline, DW.Anchor anchor)
{
if (inline != null)
{
return "inline";
}
if (anchor == null)
{
return "unknown";
}
if (anchor.Elements<DW.WrapSquare>().Any())
{
return "square";
}
if (anchor.Elements<DW.WrapTight>().Any())
{
return "tight";
}
if (anchor.Elements<DW.WrapThrough>().Any())
{
return "through";
}
if (anchor.Elements<DW.WrapTopBottom>().Any())
{
return "topBottom";
}
if (anchor.Elements<DW.WrapNone>().Any())
{
return "none";
}
return "anchor";
}
private static string GetVmlRelationshipId(V.ImageData imageData)
{
if (imageData == null)
{
return null;
}
return imageData.RelationshipId == null || string.IsNullOrWhiteSpace(imageData.RelationshipId.Value)
? imageData.RelId == null ? null : imageData.RelId.Value
: imageData.RelationshipId.Value;
}
private static string GetContainerKind(OpenXmlElement containerElement)
{
if (containerElement is W.EmbeddedObject)
{
return "embeddedObject";
}
if (containerElement is W.Picture)
{
return "vmlPicture";
}
return "unknown";
}
private static string GetEmbeddedProgramId(OpenXmlElement containerElement)
{
var oleObject = GetEmbeddedOleObjectElement(containerElement);
return GetOpenXmlAttributeValue(oleObject, "ProgID");
}
private static string GetEmbeddedPackageExtension(OpenXmlPartContainer partContainer, OpenXmlElement containerElement)
{
var oleObject = GetEmbeddedOleObjectElement(containerElement);
var relationshipId = GetOpenXmlAttributeValue(oleObject, "id");
if (string.IsNullOrWhiteSpace(relationshipId) || partContainer == null)
{
return null;
}
try
{
var part = partContainer.GetPartById(relationshipId);
return NormalizePackageExtension(part == null ? null : Path.GetExtension(part.Uri.ToString()));
}
catch
{
return null;
}
}
private static OpenXmlElement GetEmbeddedOleObjectElement(OpenXmlElement containerElement)
{
return containerElement == null
? null
: containerElement
.Descendants()
.FirstOrDefault(item => string.Equals(item.LocalName, "OLEObject", StringComparison.OrdinalIgnoreCase));
}
private static string GetOpenXmlAttributeValue(OpenXmlElement element, string localName)
{
if (element == null || string.IsNullOrWhiteSpace(localName))
{
return null;
}
var attributes = element.GetAttributes();
if (attributes == null)
{
return null;
}
foreach (var attribute in attributes)
{
if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(attribute.Value))
{
return attribute.Value;
}
}
return null;
}
private static string NormalizePackageExtension(string extension)
{
if (string.IsNullOrWhiteSpace(extension))
{
return null;
}
var normalized = extension.Trim();
if (!normalized.StartsWith(".", StringComparison.Ordinal))
{
normalized = "." + normalized;
}
return normalized.ToLowerInvariant();
}
private static IDictionary<string, string> ParseVmlStyle(string style)
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(style))
{
return map;
}
var segments = style.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
var pair = segment.Split(new[] { ':' }, 2);
if (pair.Length != 2)
{
continue;
}
var key = (pair[0] ?? string.Empty).Trim();
var value = (pair[1] ?? string.Empty).Trim();
if (key.Length == 0)
{
continue;
}
map[key] = value;
}
return map;
}
private static long ParseVmlDimensionToEmu(IDictionary<string, string> styleMap, string key)
{
if (styleMap == null || !styleMap.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue))
{
return 0L;
}
var match = Regex.Match(rawValue.Trim(), @"^(?<value>-?\d+(?:\.\d+)?)(?<unit>[a-z%]*)$", RegexOptions.IgnoreCase);
if (!match.Success)
{
return 0L;
}
if (!double.TryParse(match.Groups["value"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var numericValue))
{
return 0L;
}
var unit = (match.Groups["unit"].Value ?? string.Empty).Trim().ToLowerInvariant();
double inches;
switch (unit)
{
case "":
case "px":
inches = numericValue / 96d;
break;
case "pt":
inches = numericValue / 72d;
break;
case "pc":
inches = numericValue / 6d;
break;
case "in":
inches = numericValue;
break;
case "cm":
inches = numericValue / 2.54d;
break;
case "mm":
inches = numericValue / 25.4d;
break;
case "twip":
case "dxa":
inches = numericValue / 1440d;
break;
default:
return 0L;
}
return (long)Math.Round(inches * 914400d);
}
private static double ParseVmlRotation(IDictionary<string, string> styleMap)
{
if (styleMap == null || !styleMap.TryGetValue("rotation", out var rawValue) || string.IsNullOrWhiteSpace(rawValue))
{
return 0d;
}
var match = Regex.Match(rawValue.Trim(), @"-?\d+(?:\.\d+)?", RegexOptions.IgnoreCase);
if (!match.Success)
{
return 0d;
}
return double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var numericValue)
? numericValue
: 0d;
}
private static string DetermineVmlWrapMode(IDictionary<string, string> styleMap, OpenXmlElement containerElement)
{
var wrapType = GetVmlWordWrapType(containerElement);
if (!string.IsNullOrWhiteSpace(wrapType))
{
var mapped = NormalizeVmlWrapMode(wrapType);
if (!string.IsNullOrWhiteSpace(mapped))
{
return mapped;
}
}
var styleWrapMode = NormalizeVmlWrapMode(styleMap != null && styleMap.TryGetValue("mso-wrap-style", out var wrapStyle)
? wrapStyle
: null);
if (!string.IsNullOrWhiteSpace(styleWrapMode))
{
return styleWrapMode;
}
if (styleMap != null && styleMap.TryGetValue("position", out var position) && !string.IsNullOrWhiteSpace(position))
{
if (string.Equals(position.Trim(), "absolute", StringComparison.OrdinalIgnoreCase))
{
return "anchor";
}
}
return "inline";
}
private static string NormalizeVmlWrapMode(string wrapValue)
{
if (!string.IsNullOrWhiteSpace(wrapValue))
{
var normalized = wrapValue.Trim().ToLowerInvariant();
switch (normalized)
{
case "square":
return "square";
case "tight":
return "tight";
case "through":
return "through";
case "topandbottom":
case "top-bottom":
case "topbottom":
return "topBottom";
case "none":
return "none";
case "inline":
return "inline";
case "anchor":
return "anchor";
}
}
return null;
}
private static string GetVmlWordWrapType(OpenXmlElement containerElement)
{
if (containerElement == null)
{
return null;
}
foreach (var descendant in containerElement.Descendants())
{
if (!string.Equals(descendant.LocalName, "wrap", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!string.Equals(descendant.NamespaceUri ?? string.Empty, "urn:schemas-microsoft-com:office:word", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var attributes = descendant.GetAttributes();
if (attributes == null)
{
continue;
}
var typeAttribute = attributes.FirstOrDefault(item => string.Equals(item.LocalName, "type", StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(typeAttribute.Value))
{
return typeAttribute.Value;
}
}
return null;
}
private static string DetermineVmlObjectType(IDictionary<string, string> styleMap, OpenXmlElement containerElement)
{
var wrapMode = DetermineVmlWrapMode(styleMap, containerElement);
return string.Equals(wrapMode, "inline", StringComparison.OrdinalIgnoreCase)
? "inlineImage"
: "floatingImage";
}
private static bool IsTextRelevantPart(OpenXmlPart part)
{
return part is HeaderPart
|| part is FooterPart
|| part is WordprocessingCommentsPart
|| part is FootnotesPart
|| part is EndnotesPart
|| part.GetType().Name.IndexOf("ChartPart", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string GetStoryType(OpenXmlPart part)
{
if (part is HeaderPart)
{
return "header";
}
if (part is FooterPart)
{
return "footer";
}
if (part is WordprocessingCommentsPart)
{
return "comment";
}
if (part is FootnotesPart)
{
return "footnote";
}
if (part is EndnotesPart)
{
return "endnote";
}
if (part.GetType().Name.IndexOf("ChartPart", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "chart";
}
return "main";
}
private static void ProcessTextReplacements(
WordprocessingDocument document,
IList<RuleDefinition> rules,
DiffPayload payload,
ISet<OpenXmlPartRootElement> touchedRoots,
IProgress<FileProcessProgress> progress,
string filePath,
CancellationToken cancellationToken)
{
var operationIndex = 1;
foreach (var partContext in EnumerateTextParts(document))
{
var textTargets = EnumerateTextTargets(partContext).ToList();
for (var paragraphListIndex = 0; paragraphListIndex < textTargets.Count; paragraphListIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
var target = textTargets[paragraphListIndex];
var paragraphTouched = false;
for (var ruleIndex = 0; ruleIndex < rules.Count; ruleIndex++)
{
var rule = rules[ruleIndex];
progress?.Report(new FileProcessProgress
{
FilePath = filePath,
FilePercent = rules.Count == 0 ? 100 : Math.Min(99, (ruleIndex * 100) / rules.Count),
CurrentRuleId = rule.Id,
CurrentRuleName = rule.Name,
CurrentFileTextCount = payload.Operations.Count(item => string.Equals(item.Type, "text", StringComparison.Ordinal)),
TotalTextCount = payload.Operations.Count(item => string.Equals(item.Type, "text", StringComparison.Ordinal)),
Message = "正在替换规则 " + (rule.Name ?? rule.Id ?? "-"),
});
var state = BuildParagraphState(target);
if (string.IsNullOrEmpty(state.Text))
{
continue;
}
var matches = FindMatches(state, rule);
if (matches.Count == 0)
{
continue;
}
for (var matchIndex = matches.Count - 1; matchIndex >= 0; matchIndex--)
{
var match = matches[matchIndex];
var position = BuildTextPosition(state, match);
var originalWidth = DisplayWidthUtility.Measure(match.OriginalText);
var truncatedText = DisplayWidthUtility.TruncateToWidth(match.ReplacementTemplateResult, originalWidth, out var truncated);
var writtenText = DisplayWidthUtility.PadToWidth(truncatedText, originalWidth, out var padded);
RewriteRange(state, match.StartIndex, match.Length, writtenText, updateFont: true);
var currentState = BuildParagraphState(target);
position.ParagraphTextLength = currentState.Text == null ? 0 : currentState.Text.Length;
position.ParagraphTextHash = BuildParagraphContentHash(currentState.Text);
payload.Operations.Add(new DiffOperation
{
OperationId = "T" + operationIndex.ToString("000000"),
Type = "text",
ObjectType = DetermineTextObjectType(target, state, match, position),
Position = position,
Rule = new DiffRuleHitInfo
{
Id = rule.Id,
Name = rule.Name,
MatchMode = rule.MatchMode == RuleMatchMode.RegularExpression ? "regex" : "plain",
Pattern = rule.SearchText,
HitIndex = matches.Count - matchIndex,
},
OriginalText = match.OriginalText,
ReplacementTemplateResult = match.ReplacementTemplateResult,
WrittenText = writtenText,
DisplayWidthOriginal = originalWidth,
DisplayWidthWritten = DisplayWidthUtility.Measure(writtenText),
Truncated = truncated,
PaddingApplied = padded,
});
operationIndex++;
paragraphTouched = true;
}
}
if (paragraphTouched)
{
touchedRoots.Add(partContext.Root);
}
}
}
progress?.Report(new FileProcessProgress
{
FilePath = filePath,
FilePercent = 100,
TotalTextCount = payload.Operations.Count(item => string.Equals(item.Type, "text", StringComparison.Ordinal)),
CurrentFileTextCount = payload.Operations.Count(item => string.Equals(item.Type, "text", StringComparison.Ordinal)),
Message = "文本替换完成",
});
}
private static void RestoreText(
WordprocessingDocument document,
DiffPayload payload,
IProgress<FileProcessProgress> progress,
string filePath,
CancellationToken cancellationToken,
ISet<OpenXmlPartRootElement> touchedRoots)
{
var textTargets = EnumerateTextParts(document)
.SelectMany(EnumerateTextTargets)
.ToList();
var paragraphLookup = textTargets.ToDictionary(
target => BuildParagraphLookupKey(target.StoryType, target.ContainerPath, target.ParagraphIndex),
target => target,
StringComparer.OrdinalIgnoreCase);
var paragraphScopeLookup = textTargets
.GroupBy(target => BuildParagraphScopeKey(target.StoryType, target.ContainerPath), StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
var operations = payload.Operations
.Where(item => string.Equals(item.Type, "text", StringComparison.Ordinal))
.Reverse()
.ToList();
for (var index = 0; index < operations.Count; index++)
{
cancellationToken.ThrowIfCancellationRequested();
var operation = operations[index];
var key = BuildParagraphLookupKey(operation.Position.StoryType, operation.Position.ContainerPath, operation.Position.ParagraphIndex);
var scopeKey = BuildParagraphScopeKey(operation.Position.StoryType, operation.Position.ContainerPath);
ParagraphTarget target;
ParagraphState state;
TextRange range = null;
string actualWritten = null;
var fallbackState = default(ParagraphState);
if (paragraphLookup.TryGetValue(key, out var exactTarget))
{
fallbackState = BuildParagraphState(exactTarget);
if (TryResolveOperationInParagraph(fallbackState, operation, out range, out actualWritten)
&& ShouldTrustExactRestoreCandidate(operation, fallbackState, range))
{
target = exactTarget;
state = fallbackState;
}
else
{
target = null;
state = null;
}
}
else
{
target = null;
state = null;
}
if (target == null)
{
if (!TryResolveRestoreTargetFallback(paragraphScopeLookup, scopeKey, operation, out target, out state, out range, out actualWritten, out var ambiguous))
{
if (ambiguous)
{
throw new InvalidDataException("恢复失败:同一容器内定位到多个候选段落。 " + BuildRestoreOperationContext(operation, key, fallbackState));
}
if (fallbackState == null)
{
throw new InvalidDataException("恢复失败:未找到匹配的文本位置。 " + BuildRestoreOperationContext(operation, key, null));
}
throw new InvalidDataException(
"恢复失败:当前位置文本与记录不匹配,且未找到可回退段落。 "
+ BuildRestoreOperationContext(operation, key, fallbackState)
+ "; 记录写入文本="
+ (operation.WrittenText ?? string.Empty)
+ "; 当前文本="
+ ExtractTextRange(fallbackState, TryCreateDiagnosticRange(fallbackState, operation)));
}
}
RewriteRange(state, range.StartIndex, range.Length, operation.OriginalText ?? string.Empty);
touchedRoots.Add(target.PartContext.Root);
progress?.Report(new FileProcessProgress
{
FilePath = filePath,
FilePercent = operations.Count == 0 ? 100 : Math.Min(100, ((index + 1) * 100) / operations.Count),
CurrentRuleId = operation.Rule == null ? null : operation.Rule.Id,
CurrentRuleName = operation.Rule == null ? null : operation.Rule.Name,
CurrentFileTextCount = index + 1,
TotalTextCount = operations.Count,
Message = "正在恢复文本操作 " + operation.OperationId,
});
}
}
private static void ProcessImageReplacements(WordprocessingDocument document, DiffPayload payload, int? fixedImageSeed, CancellationToken cancellationToken)
{
var operationIndex = payload.Operations.Count(item => string.Equals(item.Type, "image", StringComparison.Ordinal)) + 1;
var processedByRelation = new Dictionary<string, ProcessedImageResult>(StringComparer.OrdinalIgnoreCase);
foreach (var image in EnumerateImageParts(document))
{
cancellationToken.ThrowIfCancellationRequested();
var extension = (Path.GetExtension(image.PartUri) ?? string.Empty).ToLowerInvariant();
var isRaster = SupportedRasterExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
var isNonBitmap = SupportedNonBitmapExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
if (!isRaster && !isNonBitmap)
{
continue;
}
if (!processedByRelation.TryGetValue(image.RelationKey, out var processed))
{
processed = isRaster
? ProcessBitmapImage(image, extension, fixedImageSeed)
: ProcessNonBitmapImage(image, extension, fixedImageSeed);
processedByRelation[image.RelationKey] = processed;
}
payload.Operations.Add(new DiffOperation
{
OperationId = "I" + operationIndex.ToString("000000"),
Type = "image",
ObjectType = image.ObjectType,
ContainerKind = image.ContainerKind,
EmbeddedProgramId = image.EmbeddedProgramId,
EmbeddedPackageExtension = image.EmbeddedPackageExtension,
Position = new DiffPosition
{
StoryType = image.StoryType,
ContainerPath = image.ContainerPath,
ParagraphIndex = 0,
StartRunIndex = 0,
StartCharOffset = 0,
EndRunIndex = 0,
EndCharOffset = 0,
ObjectIndex = image.ObjectIndex,
},
SourceKind = processed.SourceKind,
OriginalImage = processed.OriginalImage,
ConvertedBitmapImage = processed.ConvertedBitmapImage,
NewImage = processed.NewImage,
Seed = processed.Seed,
NoiseSummary = new DiffNoiseSummary
{
ModifiedPixelRatio = processed.ModifiedPixelRatio,
},
Layout = image.Layout,
});
operationIndex++;
}
}
private static ProcessedImageResult ProcessBitmapImage(ImageContext image, string extension, int? fixedImageSeed)
{
var originalBytes = ReadImageBytes(image.ImagePart);
var stableKey = image.ContainerPath ?? image.PartUri ?? image.RelationKey;
var seed = fixedImageSeed.HasValue ? DeriveFixedSeed(fixedImageSeed.Value, stableKey) : CreateRandomSeed();
DiffImageData originalImage;
DiffImageData newImage;
double ratio;
using (var originalStream = new MemoryStream(originalBytes))
using (var loadedImage = Image.FromStream(originalStream, true, true))
using (var bitmap = new Bitmap(loadedImage))
{
ratio = ApplyNoise(bitmap, seed);
newImage = CreateBitmapImageData(bitmap, extension);
originalImage = CreateDiffImageData(extension, bitmap.Width, bitmap.Height, originalBytes);
}
WriteImageData(image, Convert.FromBase64String(newImage.Data), newImage.Format);
return new ProcessedImageResult
{
SourceKind = "bitmap",
OriginalImage = originalImage,
NewImage = newImage,
ModifiedPixelRatio = ratio,
Seed = seed,
};
}
private static ProcessedImageResult ProcessNonBitmapImage(ImageContext image, string extension, int? fixedImageSeed)
{
var originalBytes = ReadImageBytes(image.ImagePart);
var stableKey = image.ContainerPath ?? image.PartUri ?? image.RelationKey;
var seed = fixedImageSeed.HasValue ? DeriveFixedSeed(fixedImageSeed.Value, stableKey) : CreateRandomSeed();
var bitmapFormat = GetNonBitmapTargetFormat(image);
var convertedBitmapImage = RasterizeNonBitmapImage(image, extension, originalBytes, bitmapFormat);
DiffImageData newImage;
double ratio;
using (var convertedStream = new MemoryStream(Convert.FromBase64String(convertedBitmapImage.Data)))
using (var loadedImage = Image.FromStream(convertedStream, true, true))
using (var bitmap = new Bitmap(loadedImage))
{
ratio = ApplyNoise(bitmap, seed);
newImage = CreateBitmapImageData(bitmap, bitmapFormat);
}
WriteImageData(image, Convert.FromBase64String(newImage.Data), newImage.Format);
return new ProcessedImageResult
{
SourceKind = "nonBitmap",
OriginalImage = CreateDiffImageData(extension, convertedBitmapImage.WidthPx, convertedBitmapImage.HeightPx, originalBytes),
ConvertedBitmapImage = convertedBitmapImage,
NewImage = newImage,
ModifiedPixelRatio = ratio,
Seed = seed,
};
}
private static void RestoreImages(WordprocessingDocument document, DiffPayload payload, CancellationToken cancellationToken)
{
var images = EnumerateImageParts(document).ToList();
var imageBytesCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var processedRelations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var operations = payload.Operations
.Where(item => string.Equals(item.Type, "image", StringComparison.Ordinal))
.Reverse()
.ToList();
foreach (var operation in operations)
{
cancellationToken.ThrowIfCancellationRequested();
if (operation.Position == null || string.IsNullOrWhiteSpace(operation.Position.ContainerPath))
{
throw new InvalidDataException("图像恢复缺少位置。");
}
var resolution = ResolveImageRestoreTarget(images, operation, imageBytesCache, processedRelations);
if (resolution == null || resolution.Image == null)
{
throw new InvalidDataException("未找到可恢复的图像容器。 " + BuildImageRestoreContext(operation, null, images.Count));
}
var image = resolution.Image;
if (processedRelations.Contains(image.RelationKey))
{
continue;
}
if (operation.Layout != null && !resolution.LayoutMatches)
{
throw new InvalidDataException("图像恢复失败:当前图像布局与记录不匹配。 " + BuildImageRestoreContext(operation, resolution, images.Count));
}
if (operation.NewImage != null && !resolution.SkipContentVerification)
{
var actual = GetImageDataBase64(image, imageBytesCache);
if (!string.Equals(actual, operation.NewImage.Data, StringComparison.Ordinal))
{
throw new InvalidDataException("图像恢复失败:当前图像内容与记录不匹配。 " + BuildImageRestoreContext(operation, resolution, images.Count));
}
}
var restoreImage = string.Equals(operation.SourceKind, "nonBitmap", StringComparison.OrdinalIgnoreCase)
? operation.ConvertedBitmapImage
: operation.OriginalImage;
if (restoreImage == null || string.IsNullOrWhiteSpace(restoreImage.Data))
{
throw new InvalidDataException("图像恢复缺少原始图像数据。");
}
WriteImageData(image, Convert.FromBase64String(restoreImage.Data), restoreImage.Format);
processedRelations.Add(image.RelationKey);
}
}
private static ImageRestoreCandidate ResolveImageRestoreTarget(
IList<ImageContext> images,
DiffOperation operation,
IDictionary<string, string> imageBytesCache,
ISet<string> processedRelations)
{
if (images == null || images.Count == 0 || operation == null || operation.Position == null)
{
return null;
}
var exactPathCandidate = images
.Where(item => string.Equals(item.ContainerPath, operation.Position.ContainerPath, StringComparison.OrdinalIgnoreCase))
.Select(item => BuildImageRestoreCandidate(item, operation, imageBytesCache, processedRelations))
.OrderBy(item => item.IsProcessed ? 1 : 0)
.ThenByDescending(item => item.ContentMatches)
.ThenByDescending(item => item.ContainerKindMatches)
.ThenByDescending(item => item.EmbeddedProgramMatches)
.ThenByDescending(item => item.EmbeddedPackageMatches)
.ThenByDescending(item => item.ObjectTypeMatches)
.ThenByDescending(item => item.LayoutMatches)
.FirstOrDefault();
if (exactPathCandidate != null && exactPathCandidate.ContentMatches)
{
return exactPathCandidate;
}
if (CanUseStructuralImageRestoreFallback(exactPathCandidate, operation, exactPath: true))
{
exactPathCandidate.SkipContentVerification = true;
return exactPathCandidate;
}
var candidates = images
.Select(item => BuildImageRestoreCandidate(item, operation, imageBytesCache, processedRelations))
.Where(item =>
item.StoryMatches
&& item.ObjectTypeCompatible
&& item.LayoutMatches
&& item.ContentMatches
&& item.ContainerKindMatches
&& item.EmbeddedProgramMatches
&& item.EmbeddedPackageMatches)
.OrderBy(item => item.IsProcessed ? 1 : 0)
.ThenByDescending(item => item.ContainerKindMatches)
.ThenByDescending(item => item.EmbeddedProgramMatches)
.ThenByDescending(item => item.EmbeddedPackageMatches)
.ThenByDescending(item => item.ObjectTypeMatches)
.ThenByDescending(item => item.PathSimilarity)
.ThenBy(item => item.ObjectIndexDistance)
.ThenBy(item => item.Image.ContainerPath ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToList();
if (candidates.Count == 0)
{
return ResolveStructuralImageRestoreTarget(images, operation, imageBytesCache, processedRelations) ?? exactPathCandidate;
}
var top = candidates[0];
var ambiguous = candidates
.Where(item =>
item.IsProcessed == top.IsProcessed
&& item.PathSimilarity == top.PathSimilarity
&& item.ObjectIndexDistance == top.ObjectIndexDistance
&& !string.Equals(item.Image.RelationKey, top.Image.RelationKey, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
if (ambiguous != null)
{
throw new InvalidDataException("图像恢复失败:存在多个等价候选图像,无法唯一定位。 " + BuildImageRestoreContext(operation, top, images.Count));
}
return top;
}
private static ImageRestoreCandidate ResolveStructuralImageRestoreTarget(
IList<ImageContext> images,
DiffOperation operation,
IDictionary<string, string> imageBytesCache,
ISet<string> processedRelations)
{
var candidates = images
.Select(item => BuildImageRestoreCandidate(item, operation, imageBytesCache, processedRelations))
.Where(item => CanUseStructuralImageRestoreFallback(item, operation, exactPath: false))
.OrderBy(item => item.IsProcessed ? 1 : 0)
.ThenByDescending(item => item.ObjectTypeMatches)
.ThenByDescending(item => item.PathSimilarity)
.ThenBy(item => item.ObjectIndexDistance)
.ThenBy(item => item.Image.ContainerPath ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToList();
if (candidates.Count == 0)
{
return null;
}
var top = candidates[0];
var ambiguous = candidates
.Where(item =>
item.IsProcessed == top.IsProcessed
&& item.ObjectTypeMatches == top.ObjectTypeMatches
&& item.PathSimilarity == top.PathSimilarity
&& item.ObjectIndexDistance == top.ObjectIndexDistance
&& !string.Equals(item.Image.RelationKey, top.Image.RelationKey, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
if (ambiguous != null)
{
throw new InvalidDataException("图像恢复失败:存在多个结构等价候选图像,无法唯一定位。 " + BuildImageRestoreContext(operation, top, images.Count));
}
top.SkipContentVerification = true;
return top;
}
private static bool CanUseStructuralImageRestoreFallback(ImageRestoreCandidate candidate, DiffOperation operation, bool exactPath)
{
if (candidate == null || candidate.Image == null || operation == null)
{
return false;
}
if (!IsVmlRestoreCandidate(candidate, operation))
{
return false;
}
if (!candidate.StoryMatches
|| !candidate.ContainerKindMatches
|| !candidate.EmbeddedProgramMatches
|| !candidate.EmbeddedPackageMatches
|| !candidate.ObjectTypeCompatible
|| !candidate.LayoutMatches)
{
return false;
}
return exactPath
|| candidate.ObjectIndexDistance <= 2
|| candidate.PathSimilarity >= 400;
}
private static bool IsVmlRestoreCandidate(ImageRestoreCandidate candidate, DiffOperation operation)
{
var expectedPath = operation == null || operation.Position == null ? null : operation.Position.ContainerPath;
return IsVmlImageContainerKind(candidate.Image.ContainerKind)
|| IsVmlImageContainerKind(operation == null ? null : operation.ContainerKind)
|| IsVmlLikeContainerPath(candidate.Image.ContainerPath)
|| IsVmlLikeContainerPath(expectedPath);
}
private static bool IsVmlImageContainerKind(string containerKind)
{
return string.Equals(containerKind, "vmlPicture", StringComparison.OrdinalIgnoreCase)
|| string.Equals(containerKind, "embeddedObject", StringComparison.OrdinalIgnoreCase);
}
private static ImageRestoreCandidate BuildImageRestoreCandidate(
ImageContext image,
DiffOperation operation,
IDictionary<string, string> imageBytesCache,
ISet<string> processedRelations)
{
var expectedStoryType = operation.Position == null ? null : operation.Position.StoryType;
var expectedObjectType = operation.ObjectType;
var expectedLayout = operation.Layout;
var expectedImageData = operation.NewImage == null ? null : operation.NewImage.Data;
var expectedPath = operation.Position == null ? null : operation.Position.ContainerPath;
var expectedObjectIndex = operation.Position == null ? null : operation.Position.ObjectIndex;
var expectedContainerKind = operation.ContainerKind;
var expectedEmbeddedProgramId = operation.EmbeddedProgramId;
var expectedEmbeddedPackageExtension = NormalizePackageExtension(operation.EmbeddedPackageExtension);
var objectTypeMatches = string.IsNullOrWhiteSpace(expectedObjectType)
|| string.Equals(image.ObjectType ?? string.Empty, expectedObjectType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
var objectTypeCompatible = objectTypeMatches
|| IsCompatibleImageObjectType(expectedObjectType, image.ObjectType, expectedPath, image.ContainerPath);
var allowCompatibleLayout = (objectTypeCompatible && !objectTypeMatches)
|| IsVmlImageContainerKind(expectedContainerKind)
|| IsVmlImageContainerKind(image.ContainerKind)
|| IsVmlLikeContainerPath(expectedPath)
|| IsVmlLikeContainerPath(image.ContainerPath);
var containerKindMatches = string.IsNullOrWhiteSpace(expectedContainerKind)
|| string.Equals(image.ContainerKind ?? string.Empty, expectedContainerKind ?? string.Empty, StringComparison.OrdinalIgnoreCase);
var embeddedProgramMatches = string.IsNullOrWhiteSpace(expectedEmbeddedProgramId)
|| string.Equals(image.EmbeddedProgramId ?? string.Empty, expectedEmbeddedProgramId ?? string.Empty, StringComparison.OrdinalIgnoreCase);
var embeddedPackageMatches = string.IsNullOrWhiteSpace(expectedEmbeddedPackageExtension)
|| string.Equals(
NormalizePackageExtension(image.EmbeddedPackageExtension) ?? string.Empty,
expectedEmbeddedPackageExtension ?? string.Empty,
StringComparison.OrdinalIgnoreCase);
return new ImageRestoreCandidate
{
Image = image,
IsProcessed = processedRelations != null && processedRelations.Contains(image.RelationKey),
StoryMatches = string.IsNullOrWhiteSpace(expectedStoryType)
|| string.Equals(image.StoryType ?? string.Empty, expectedStoryType ?? string.Empty, StringComparison.OrdinalIgnoreCase),
ContainerKindMatches = containerKindMatches,
EmbeddedProgramMatches = embeddedProgramMatches,
EmbeddedPackageMatches = embeddedPackageMatches,
ObjectTypeMatches = objectTypeMatches,
ObjectTypeCompatible = objectTypeCompatible,
LayoutMatches = expectedLayout == null || IsLayoutEquivalent(image.Layout, expectedLayout, allowCompatibleLayout),
ContentMatches = string.IsNullOrWhiteSpace(expectedImageData)
|| string.Equals(GetImageDataBase64(image, imageBytesCache), expectedImageData, StringComparison.Ordinal),
PathSimilarity = ComputeContainerPathSimilarity(image.ContainerPath, expectedPath),
ObjectIndexDistance = ComputeObjectIndexDistance(image.ObjectIndex, expectedObjectIndex),
};
}
private static string GetImageDataBase64(ImageContext image, IDictionary<string, string> imageBytesCache)
{
if (image == null)
{
return string.Empty;
}
var cacheKey = image.RelationKey ?? image.ContainerPath ?? image.PartUri ?? Guid.NewGuid().ToString("N");
if (!imageBytesCache.TryGetValue(cacheKey, out var data))
{
data = Convert.ToBase64String(ReadImageBytes(image.ImagePart));
imageBytesCache[cacheKey] = data;
}
return data;
}
private static int ComputeContainerPathSimilarity(string actualPath, string expectedPath)
{
if (string.IsNullOrWhiteSpace(actualPath) || string.IsNullOrWhiteSpace(expectedPath))
{
return 0;
}
if (string.Equals(actualPath, expectedPath, StringComparison.OrdinalIgnoreCase))
{
return int.MaxValue;
}
var actualSegments = actualPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var expectedSegments = expectedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var length = Math.Min(actualSegments.Length, expectedSegments.Length);
var score = 0;
for (var index = 0; index < length; index++)
{
if (!string.Equals(actualSegments[index], expectedSegments[index], StringComparison.OrdinalIgnoreCase))
{
break;
}
score += 100;
}
return score;
}
private static int ComputeObjectIndexDistance(int actualIndex, int? expectedIndex)
{
if (!expectedIndex.HasValue)
{
return 0;
}
return Math.Abs(actualIndex - expectedIndex.Value);
}
private static string BuildImageRestoreContext(DiffOperation operation, ImageRestoreCandidate candidate, int imageCount)
{
var builder = new StringBuilder();
builder.Append("OperationId=").Append(operation == null ? string.Empty : operation.OperationId ?? string.Empty);
builder.Append("; ObjectType=").Append(operation == null ? string.Empty : operation.ObjectType ?? string.Empty);
builder.Append("; ContainerKind=").Append(operation == null ? string.Empty : operation.ContainerKind ?? string.Empty);
builder.Append("; EmbeddedProgramId=").Append(operation == null ? string.Empty : operation.EmbeddedProgramId ?? string.Empty);
builder.Append("; EmbeddedPackageExtension=").Append(operation == null ? string.Empty : operation.EmbeddedPackageExtension ?? string.Empty);
builder.Append("; StoryType=").Append(operation == null || operation.Position == null ? string.Empty : operation.Position.StoryType ?? string.Empty);
builder.Append("; Position=").Append(BuildDiffPositionSummary(operation == null ? null : operation.Position));
builder.Append("; TotalImages=").Append(imageCount.ToString(CultureInfo.InvariantCulture));
if (candidate != null && candidate.Image != null)
{
builder.Append("; CandidatePath=").Append(candidate.Image.ContainerPath ?? string.Empty);
builder.Append("; CandidateRelation=").Append(candidate.Image.RelationKey ?? string.Empty);
builder.Append("; CandidateContainerKind=").Append(candidate.Image.ContainerKind ?? string.Empty);
builder.Append("; CandidateEmbeddedProgramId=").Append(candidate.Image.EmbeddedProgramId ?? string.Empty);
builder.Append("; CandidateEmbeddedPackageExtension=").Append(candidate.Image.EmbeddedPackageExtension ?? string.Empty);
builder.Append("; CandidateObjectType=").Append(candidate.Image.ObjectType ?? string.Empty);
builder.Append("; CandidateWrapMode=").Append(candidate.Image.Layout == null ? string.Empty : candidate.Image.Layout.WrapMode ?? string.Empty);
builder.Append("; CandidateObjectIndex=").Append(candidate.Image.ObjectIndex.ToString(CultureInfo.InvariantCulture));
builder.Append("; CandidateStoryMatch=").Append(candidate.StoryMatches ? "Y" : "N");
builder.Append("; CandidateContainerKindMatch=").Append(candidate.ContainerKindMatches ? "Y" : "N");
builder.Append("; CandidateEmbeddedProgramMatch=").Append(candidate.EmbeddedProgramMatches ? "Y" : "N");
builder.Append("; CandidateEmbeddedPackageMatch=").Append(candidate.EmbeddedPackageMatches ? "Y" : "N");
builder.Append("; CandidateObjectTypeMatch=").Append(candidate.ObjectTypeMatches ? "Y" : "N");
builder.Append("; CandidateObjectTypeCompatible=").Append(candidate.ObjectTypeCompatible ? "Y" : "N");
builder.Append("; CandidateLayoutMatch=").Append(candidate.LayoutMatches ? "Y" : "N");
builder.Append("; CandidateContentMatch=").Append(candidate.ContentMatches ? "Y" : "N");
builder.Append("; CandidatePathSimilarity=").Append(candidate.PathSimilarity == int.MaxValue ? "Exact" : candidate.PathSimilarity.ToString(CultureInfo.InvariantCulture));
builder.Append("; CandidateObjectIndexDistance=").Append(candidate.ObjectIndexDistance.ToString(CultureInfo.InvariantCulture));
}
return builder.ToString();
}
private static string BuildDiffPositionSummary(DiffPosition position)
{
if (position == null)
{
return "<null>";
}
return string.Format(
CultureInfo.InvariantCulture,
"story={0},container={1},paragraph={2},objectIndex={3},textStart={4},textEnd={5},runStart={6}:{7},runEnd={8}:{9}",
position.StoryType ?? string.Empty,
position.ContainerPath ?? string.Empty,
position.ParagraphIndex,
position.ObjectIndex.HasValue ? position.ObjectIndex.Value.ToString(CultureInfo.InvariantCulture) : "<null>",
position.StartTextOffset,
position.EndTextOffset,
position.StartRunIndex,
position.StartCharOffset,
position.EndRunIndex,
position.EndCharOffset);
}
private static byte[] ReadImageBytes(ImagePart imagePart)
{
using (var stream = imagePart.GetStream(FileMode.Open, FileAccess.Read))
using (var memory = new MemoryStream())
{
stream.CopyTo(memory);
return memory.ToArray();
}
}
private static string GetNonBitmapTargetFormat(ImageContext image)
{
return IsVisioEmbeddedObject(image) ? ".bmp" : ".png";
}
private static bool IsVisioEmbeddedObject(ImageContext image)
{
if (image == null || !string.Equals(image.ContainerKind, "embeddedObject", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (IsVisioProgramId(image.EmbeddedProgramId))
{
return true;
}
var packageExtension = NormalizePackageExtension(image.EmbeddedPackageExtension);
return !string.IsNullOrWhiteSpace(packageExtension)
&& SupportedVisioPackageExtensions.Contains(packageExtension, StringComparer.OrdinalIgnoreCase);
}
private static bool IsVisioProgramId(string programId)
{
return !string.IsNullOrWhiteSpace(programId)
&& programId.Trim().StartsWith("Visio.", StringComparison.OrdinalIgnoreCase);
}
private static DiffImageData RasterizeNonBitmapImage(ImageContext image, string extension, byte[] originalBytes, string targetBitmapFormat)
{
var backgroundColor = GetNonBitmapPreviewBackground(image, extension, targetBitmapFormat);
using (var bitmap = CreateNonBitmapPreview(image, extension, originalBytes, backgroundColor))
{
return CreateBitmapImageData(bitmap, targetBitmapFormat);
}
}
private static Color GetNonBitmapPreviewBackground(ImageContext image, string extension, string targetBitmapFormat)
{
if (IsVisioEmbeddedObject(image)
|| IsMetafileExtension(extension)
|| string.Equals(NormalizeImageFormat(targetBitmapFormat), "bmp", StringComparison.OrdinalIgnoreCase))
{
return Color.White;
}
return Color.Transparent;
}
private static bool IsMetafileExtension(string extension)
{
return string.Equals(extension, ".wmf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(extension, ".emf", StringComparison.OrdinalIgnoreCase);
}
private static Bitmap CreateNonBitmapPreview(ImageContext image, string extension, byte[] originalBytes, Color backgroundColor)
{
if (string.Equals(extension, ".svg", StringComparison.OrdinalIgnoreCase))
{
using (var stream = new MemoryStream(originalBytes))
{
var svgDocument = SvgDocument.Open<SvgDocument>(stream);
if (svgDocument == null)
{
throw new InvalidDataException("SVG 图像无法解析。");
}
using (var rendered = svgDocument.Draw())
{
if (rendered == null)
{
throw new InvalidDataException("SVG 图像无法渲染。");
}
return CreateLayoutBitmap(rendered, image.Layout, backgroundColor);
}
}
}
using (var stream = new MemoryStream(originalBytes))
using (var loadedImage = Image.FromStream(stream, true, true))
{
return CreateLayoutBitmap(loadedImage, image.Layout, backgroundColor);
}
}
private static Bitmap CreateLayoutBitmap(Image image, DiffImageLayout layout, Color backgroundColor)
{
var width = ResolvePixelLength(layout == null ? 0L : layout.DisplayWidthEmu, image == null ? 0 : image.Width);
var height = ResolvePixelLength(layout == null ? 0L : layout.DisplayHeightEmu, image == null ? 0 : image.Height);
var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(backgroundColor);
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawImage(image, new Rectangle(0, 0, width, height));
}
return bitmap;
}
private static int ResolvePixelLength(long emuLength, int fallback)
{
if (emuLength > 0)
{
return Math.Max(1, (int)Math.Round(emuLength / 9525d));
}
return Math.Max(1, fallback <= 0 ? 1 : fallback);
}
private static DiffImageData CreateBitmapImageData(Bitmap bitmap, string extension)
{
using (var output = new MemoryStream())
{
var normalizedExtension = NormalizeImageFormat(extension);
bitmap.Save(output, GetImageFormat("." + normalizedExtension));
return CreateDiffImageData(normalizedExtension, bitmap.Width, bitmap.Height, output.ToArray());
}
}
private static DiffImageData CreateDiffImageData(string formatOrExtension, int width, int height, byte[] data)
{
return new DiffImageData
{
Format = NormalizeImageFormat(formatOrExtension),
WidthPx = width,
HeightPx = height,
Data = Convert.ToBase64String(data ?? Array.Empty<byte>()),
};
}
private static string NormalizeImageFormat(string formatOrExtension)
{
var normalized = (formatOrExtension ?? string.Empty).Trim().TrimStart('.').ToLowerInvariant();
switch (normalized)
{
case "jpeg":
return "jpg";
case "tiff":
return "tif";
default:
return normalized;
}
}
private static void WriteImageData(ImageContext image, byte[] bytes, string targetFormat)
{
if (image == null)
{
throw new ArgumentNullException("image");
}
var normalizedFormat = NormalizeImageFormat(targetFormat);
if (!IsImagePartFormatMatch(image.PartUri, normalizedFormat))
{
var originalPart = image.ImagePart;
image.ParentPart.DeletePart(originalPart);
image.ImagePart = CreateImagePart(image.ParentPart, GetImagePartType(normalizedFormat), image.RelationshipId);
image.PartUri = image.ImagePart.Uri.ToString();
}
using (var writeStream = image.ImagePart.GetStream(FileMode.Create, FileAccess.Write))
{
writeStream.Write(bytes, 0, bytes.Length);
}
}
private static ImagePart CreateImagePart(OpenXmlPart parentPart, PartTypeInfo partType, string relationshipId)
{
if (parentPart is MainDocumentPart mainDocumentPart)
{
return mainDocumentPart.AddImagePart(partType, relationshipId);
}
if (parentPart is HeaderPart headerPart)
{
return headerPart.AddImagePart(partType, relationshipId);
}
if (parentPart is FooterPart footerPart)
{
return footerPart.AddImagePart(partType, relationshipId);
}
if (parentPart is WordprocessingCommentsPart commentsPart)
{
return commentsPart.AddImagePart(partType, relationshipId);
}
if (parentPart is FootnotesPart footnotesPart)
{
return footnotesPart.AddImagePart(partType, relationshipId);
}
if (parentPart is EndnotesPart endnotesPart)
{
return endnotesPart.AddImagePart(partType, relationshipId);
}
if (parentPart is ChartPart chartPart)
{
return chartPart.AddImagePart(partType, relationshipId);
}
throw new NotSupportedException("Unsupported image parent part: " + parentPart.GetType().FullName);
}
private static bool IsImagePartFormatMatch(string partUri, string normalizedFormat)
{
var extension = (Path.GetExtension(partUri) ?? string.Empty).ToLowerInvariant();
switch (normalizedFormat)
{
case "jpg":
return string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase)
|| string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase);
case "tif":
return string.Equals(extension, ".tif", StringComparison.OrdinalIgnoreCase)
|| string.Equals(extension, ".tiff", StringComparison.OrdinalIgnoreCase);
default:
return string.Equals(extension, "." + normalizedFormat, StringComparison.OrdinalIgnoreCase);
}
}
private static PartTypeInfo GetImagePartType(string normalizedFormat)
{
switch (NormalizeImageFormat(normalizedFormat))
{
case "jpg":
return ImagePartType.Jpeg;
case "bmp":
return ImagePartType.Bmp;
case "gif":
return ImagePartType.Gif;
case "tif":
return ImagePartType.Tiff;
case "wmf":
return ImagePartType.Wmf;
case "emf":
return ImagePartType.Emf;
case "svg":
return ImagePartType.Svg;
default:
return ImagePartType.Png;
}
}
private static bool IsLayoutEquivalent(DiffImageLayout current, DiffImageLayout expected, bool allowCompatibleWrapModes = false)
{
if (current == null && expected == null)
{
return true;
}
if (current == null || expected == null)
{
return false;
}
var wrapModesMatch = string.Equals(current.WrapMode ?? string.Empty, expected.WrapMode ?? string.Empty, StringComparison.OrdinalIgnoreCase)
|| (allowCompatibleWrapModes && AreCompatibleVmlWrapModes(current.WrapMode, expected.WrapMode));
return AreEmuLengthsEquivalent(current.DisplayWidthEmu, expected.DisplayWidthEmu)
&& AreEmuLengthsEquivalent(current.DisplayHeightEmu, expected.DisplayHeightEmu)
&& wrapModesMatch
&& Math.Abs(current.RotationDegree - expected.RotationDegree) <= 0.1d;
}
private static bool IsCompatibleImageObjectType(string expectedObjectType, string actualObjectType, string expectedPath, string actualPath)
{
if (string.IsNullOrWhiteSpace(expectedObjectType) || string.IsNullOrWhiteSpace(actualObjectType))
{
return false;
}
if (string.Equals(expectedObjectType, actualObjectType, StringComparison.OrdinalIgnoreCase))
{
return true;
}
var inlineOrFloating = (string.Equals(expectedObjectType, "inlineImage", StringComparison.OrdinalIgnoreCase)
&& string.Equals(actualObjectType, "floatingImage", StringComparison.OrdinalIgnoreCase))
|| (string.Equals(expectedObjectType, "floatingImage", StringComparison.OrdinalIgnoreCase)
&& string.Equals(actualObjectType, "inlineImage", StringComparison.OrdinalIgnoreCase));
if (!inlineOrFloating)
{
return false;
}
return IsVmlLikeContainerPath(expectedPath) || IsVmlLikeContainerPath(actualPath);
}
private static bool AreCompatibleVmlWrapModes(string currentWrapMode, string expectedWrapMode)
{
var left = NormalizeWrapModeForCompatibility(currentWrapMode);
var right = NormalizeWrapModeForCompatibility(expectedWrapMode);
if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
{
return false;
}
if (string.Equals(left, right, StringComparison.OrdinalIgnoreCase))
{
return true;
}
var floatingModes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"inline",
"anchor",
"square",
"tight",
"through",
"topBottom",
"none",
};
return floatingModes.Contains(left) && floatingModes.Contains(right);
}
private static string NormalizeWrapModeForCompatibility(string wrapMode)
{
if (string.IsNullOrWhiteSpace(wrapMode))
{
return null;
}
var normalized = wrapMode.Trim();
if (string.Equals(normalized, "topandbottom", StringComparison.OrdinalIgnoreCase)
|| string.Equals(normalized, "top-bottom", StringComparison.OrdinalIgnoreCase))
{
return "topBottom";
}
return normalized;
}
private static bool IsVmlLikeContainerPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
return path.IndexOf("/pict[", StringComparison.OrdinalIgnoreCase) >= 0
|| path.IndexOf("/object[", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool AreEmuLengthsEquivalent(long current, long expected)
{
if (current == expected)
{
return true;
}
if (current <= 0 || expected <= 0)
{
return false;
}
var delta = Math.Abs(current - expected);
var baseline = Math.Max(current, expected);
const long absoluteToleranceEmu = 19050L;
var relativeToleranceEmu = (long)Math.Ceiling(baseline * 0.02d);
var tolerance = Math.Max(absoluteToleranceEmu, relativeToleranceEmu);
return delta <= tolerance;
}
private static void SaveTouchedRoots(ISet<OpenXmlPartRootElement> touchedRoots, WordprocessingDocument document)
{
foreach (var root in touchedRoots)
{
root.Save();
}
if (document.MainDocumentPart != null && document.MainDocumentPart.Document != null)
{
document.MainDocumentPart.Document.Save();
}
}
private static int CreateRandomSeed()
{
using (var rng = RandomNumberGenerator.Create())
{
var buffer = new byte[4];
do
{
rng.GetBytes(buffer);
var candidate = BitConverter.ToInt32(buffer, 0) & int.MaxValue;
if (candidate != 0)
{
return candidate;
}
}
while (true);
}
}
private static int DeriveFixedSeed(int fixedSeed, string stableKey)
{
var input = Encoding.UTF8.GetBytes(fixedSeed.ToString() + "::" + (stableKey ?? string.Empty));
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(input);
var seed = BitConverter.ToInt32(hash, 0) & int.MaxValue;
return seed == 0 ? 1 : seed;
}
}
private static double ApplyNoise(Bitmap bitmap, int seed)
{
if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0)
{
return 0;
}
var random = new Random(seed);
var total = bitmap.Width * bitmap.Height;
var target = Math.Min(total, (int)Math.Ceiling(total * NoiseTargetPixelRatio));
var modified = 0;
var cells = CreateNoiseCells(bitmap.Width, bitmap.Height, target, random);
foreach (var cell in cells)
{
modified += ApplyNoiseToCell(bitmap, cell, random);
}
return (double)modified / total;
}
private static IList<NoiseCell> CreateNoiseCells(int width, int height, int target, Random random)
{
var total = width * height;
var assigned = 0;
var cells = new List<NoiseCell>();
for (var y = 0; y < height; y += NoiseCellSize)
{
for (var x = 0; x < width; x += NoiseCellSize)
{
var cellWidth = Math.Min(NoiseCellSize, width - x);
var cellHeight = Math.Min(NoiseCellSize, height - y);
var area = cellWidth * cellHeight;
var exactQuota = ((double)area * target) / total;
var quota = (int)Math.Floor(exactQuota);
assigned += quota;
cells.Add(new NoiseCell
{
X = x,
Y = y,
Width = cellWidth,
Height = cellHeight,
Area = area,
Quota = quota,
Fraction = exactQuota - quota,
TieBreaker = random.NextDouble(),
});
}
}
var remaining = target - assigned;
foreach (var cell in cells.OrderByDescending(item => item.Fraction).ThenBy(item => item.TieBreaker).Take(remaining))
{
cell.Quota++;
}
return cells;
}
private static int ApplyNoiseToCell(Bitmap bitmap, NoiseCell cell, Random random)
{
if (cell.Quota <= 0)
{
return 0;
}
var indexes = Enumerable.Range(0, cell.Area).ToArray();
for (var i = 0; i < cell.Quota; i++)
{
var swapIndex = i + random.Next(cell.Area - i);
var selected = indexes[swapIndex];
indexes[swapIndex] = indexes[i];
indexes[i] = selected;
var x = cell.X + (selected % cell.Width);
var y = cell.Y + (selected / cell.Width);
var source = bitmap.GetPixel(x, y);
bitmap.SetPixel(x, y, CreateNoiseColor(source, random));
}
return cell.Quota;
}
private static Color CreateNoiseColor(Color source, Random random)
{
int r;
int g;
int b;
switch (random.Next(5))
{
case 0:
r = 0;
g = 0;
b = 0;
break;
case 1:
r = 255;
g = 255;
b = 255;
break;
case 2:
r = 255 - source.R;
g = 255 - source.G;
b = 255 - source.B;
break;
case 3:
r = random.Next(256);
g = random.Next(256);
b = random.Next(256);
break;
default:
r = source.R < 128 ? random.Next(192, 256) : random.Next(0, 64);
g = source.G < 128 ? random.Next(192, 256) : random.Next(0, 64);
b = source.B < 128 ? random.Next(192, 256) : random.Next(0, 64);
break;
}
if (r == source.R && g == source.G && b == source.B)
{
r = 255 - source.R;
g = 255 - source.G;
b = 255 - source.B;
if (r == source.R && g == source.G && b == source.B)
{
r = (r + 127) % 256;
}
}
return Color.FromArgb(source.A, r, g, b);
}
private static ImageFormat GetImageFormat(string extension)
{
switch ((extension ?? string.Empty).ToLowerInvariant())
{
case ".jpg":
case ".jpeg":
return ImageFormat.Jpeg;
case ".bmp":
return ImageFormat.Bmp;
case ".gif":
return ImageFormat.Gif;
case ".tif":
case ".tiff":
return ImageFormat.Tiff;
default:
return ImageFormat.Png;
}
}
private static ParagraphTarget BuildParagraphTarget(PartContext context, W.Paragraph paragraph)
{
var container = paragraph.Parent;
var paragraphIndex = 0;
if (container != null)
{
paragraphIndex = container.ChildElements.OfType<W.Paragraph>().TakeWhile(item => !ReferenceEquals(item, paragraph)).Count();
}
var storyType = paragraph.Ancestors().Any(item => string.Equals(item.LocalName, "txbxContent", StringComparison.OrdinalIgnoreCase))
? "textbox"
: context.StoryType;
return new ParagraphTarget
{
PartContext = context,
ParagraphElement = paragraph,
StoryType = storyType,
ContainerPath = BuildContainerPath(context.PartUri, container, context.Root),
ParagraphIndex = paragraphIndex,
};
}
private static ParagraphTarget BuildParagraphTarget(PartContext context, A.Paragraph paragraph)
{
var container = paragraph.Parent;
var paragraphIndex = 0;
if (container != null)
{
paragraphIndex = container.ChildElements.OfType<A.Paragraph>().TakeWhile(item => !ReferenceEquals(item, paragraph)).Count();
}
return new ParagraphTarget
{
PartContext = context,
ParagraphElement = paragraph,
StoryType = context.StoryType,
ContainerPath = BuildContainerPath(context.PartUri, container, context.Root),
ParagraphIndex = paragraphIndex,
};
}
private static IEnumerable<ParagraphTarget> EnumerateTextTargets(PartContext context)
{
if (context == null || context.Root == null)
{
yield break;
}
if (string.Equals(context.StoryType, "chart", StringComparison.OrdinalIgnoreCase))
{
foreach (var paragraph in context.Root.Descendants<A.Paragraph>())
{
yield return BuildParagraphTarget(context, paragraph);
}
yield break;
}
foreach (var paragraph in context.Root.Descendants<W.Paragraph>())
{
yield return BuildParagraphTarget(context, paragraph);
}
}
private static string BuildContainerPath(string partUri, OpenXmlElement container, OpenXmlElement root)
{
var builder = new StringBuilder(partUri ?? string.Empty);
var elements = new Stack<OpenXmlElement>();
var current = container ?? root;
while (current != null)
{
elements.Push(current);
if (ReferenceEquals(current, root))
{
break;
}
current = current.Parent;
}
while (elements.Count > 0)
{
var element = elements.Pop();
var parent = element.Parent;
var index = 0;
if (parent != null)
{
index = parent.ChildElements
.OfType<OpenXmlElement>()
.Where(item => string.Equals(item.LocalName, element.LocalName, StringComparison.OrdinalIgnoreCase))
.TakeWhile(item => !ReferenceEquals(item, element))
.Count();
}
builder.Append('/');
builder.Append(element.LocalName);
builder.Append('[');
builder.Append(index);
builder.Append(']');
}
return builder.ToString();
}
private static ParagraphState BuildParagraphState(ParagraphTarget target)
{
var textBuilder = new StringBuilder();
var leaves = new List<TextLeaf>();
var runList = new List<OpenXmlElement>();
var runOffsets = new Dictionary<OpenXmlElement, int>();
var logicalWordBoundaries = new HashSet<int>();
TextLeaf previousLeaf = null;
foreach (var element in target.ParagraphElement.Descendants())
{
var leaf = element as OpenXmlLeafTextElement;
if (leaf != null)
{
if (!IsSupportedTextLeaf(leaf) || !IsVisibleText(leaf))
{
continue;
}
previousLeaf = AppendLeaf(leaves, textBuilder, runList, runOffsets, logicalWordBoundaries, previousLeaf, target.ParagraphElement, element, new TextLeaf
{
TextElement = leaf,
Text = leaf.Text ?? string.Empty,
IsEquationText = IsEquationLeaf(leaf),
}) ?? previousLeaf;
continue;
}
if (!VmlTextMetadataUtility.IsVmlShapeElement(element))
{
continue;
}
foreach (var binding in VmlTextMetadataUtility.CreateBindings(element))
{
previousLeaf = AppendLeaf(leaves, textBuilder, runList, runOffsets, logicalWordBoundaries, previousLeaf, target.ParagraphElement, element, new TextLeaf
{
Text = binding.Text ?? string.Empty,
IsEquationText = binding.IsEquationText,
IsChartText = binding.IsChartText,
ApplyText = binding.Apply,
}) ?? previousLeaf;
}
}
return new ParagraphState
{
Target = target,
Text = textBuilder.ToString(),
Leaves = leaves,
LogicalWordBoundaries = logicalWordBoundaries,
};
}
private static TextLeaf AppendLeaf(
ICollection<TextLeaf> leaves,
StringBuilder textBuilder,
IList<OpenXmlElement> runList,
IDictionary<OpenXmlElement, int> runOffsets,
ISet<int> logicalWordBoundaries,
TextLeaf previousLeaf,
OpenXmlElement paragraphElement,
OpenXmlElement sourceElement,
TextLeaf leaf)
{
if (leaf == null || string.IsNullOrEmpty(leaf.Text))
{
return null;
}
var run = FindOwningRun(paragraphElement, sourceElement);
if (run == null)
{
return null;
}
if (!runOffsets.ContainsKey(run))
{
runOffsets[run] = 0;
runList.Add(run);
}
var runIndex = runList.IndexOf(run);
var runCharStart = runOffsets[run];
runOffsets[run] += leaf.Text.Length;
if (ShouldCreateLogicalWordBoundary(previousLeaf, leaf))
{
logicalWordBoundaries.Add(textBuilder.Length);
}
var globalStart = textBuilder.Length;
textBuilder.Append(leaf.Text);
leaf.Run = run;
leaf.RunIndex = runIndex;
leaf.RunCharStart = runCharStart;
leaf.GlobalStart = globalStart;
leaves.Add(leaf);
return leaf;
}
private static bool ShouldCreateLogicalWordBoundary(TextLeaf previousLeaf, TextLeaf currentLeaf)
{
return previousLeaf != null
&& currentLeaf != null
&& (previousLeaf.IsChartText || currentLeaf.IsChartText);
}
private static bool IsSupportedTextLeaf(OpenXmlLeafTextElement text)
{
return text is W.Text
|| text is M.Text
|| text is A.Text;
}
private static OpenXmlElement FindOwningRun(OpenXmlElement paragraphElement, OpenXmlElement element)
{
var current = element == null ? null : element.Parent;
while (current != null && !ReferenceEquals(current, paragraphElement))
{
if (current is W.Run || current is M.Run || current is A.Run)
{
return current;
}
current = current.Parent;
}
return null;
}
private static bool IsEquationLeaf(OpenXmlLeafTextElement text)
{
return text != null && text.Ancestors().Any(item =>
string.Equals(item.LocalName, "oMath", StringComparison.OrdinalIgnoreCase)
|| string.Equals(item.LocalName, "oMathPara", StringComparison.OrdinalIgnoreCase));
}
private static bool IsVisibleText(OpenXmlLeafTextElement text)
{
if (text is W.FieldCode || text is W.DeletedText)
{
return false;
}
if (text.Ancestors<W.DeletedRun>().Any())
{
return false;
}
var run = text.Ancestors<W.Run>().FirstOrDefault();
if (run != null && run.RunProperties != null && run.RunProperties.Vanish != null)
{
return false;
}
return true;
}
private static List<TextMatch> FindMatches(ParagraphState state, RuleDefinition rule)
{
var matches = new List<TextMatch>();
var text = state == null ? null : state.Text;
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(rule.SearchText))
{
return matches;
}
if (rule.MatchMode == RuleMatchMode.RegularExpression)
{
var pattern = rule.SearchText;
var options = RegexOptions.CultureInvariant;
if (!rule.IsCaseSensitive)
{
options |= RegexOptions.IgnoreCase;
}
foreach (Match match in Regex.Matches(text, pattern, options))
{
if (!match.Success || match.Length <= 0)
{
continue;
}
if (rule.IsWholeWord && !IsWholeWord(text, state.LogicalWordBoundaries, match.Index, match.Length))
{
continue;
}
matches.Add(new TextMatch
{
StartIndex = match.Index,
Length = match.Length,
OriginalText = match.Value,
ReplacementTemplateResult = match.Result(rule.ReplacementText ?? string.Empty),
});
}
return matches;
}
var comparison = rule.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
var start = 0;
while (start < text.Length)
{
var index = text.IndexOf(rule.SearchText, start, comparison);
if (index < 0)
{
break;
}
if (!rule.IsWholeWord || IsWholeWord(text, state.LogicalWordBoundaries, index, rule.SearchText.Length))
{
matches.Add(new TextMatch
{
StartIndex = index,
Length = rule.SearchText.Length,
OriginalText = text.Substring(index, rule.SearchText.Length),
ReplacementTemplateResult = rule.ReplacementText ?? string.Empty,
});
}
start = index + Math.Max(rule.SearchText.Length, 1);
}
return matches;
}
private static bool IsWholeWord(string text, ISet<int> logicalWordBoundaries, int start, int length)
{
var leftOk = start <= 0
|| (logicalWordBoundaries != null && logicalWordBoundaries.Contains(start))
|| GetWordCharClass(text[start - 1]) != GetWordCharClass(text[start]);
var rightIndex = start + length;
var rightOk = rightIndex >= text.Length
|| (logicalWordBoundaries != null && logicalWordBoundaries.Contains(rightIndex))
|| GetWordCharClass(text[rightIndex - 1]) != GetWordCharClass(text[rightIndex]);
return leftOk && rightOk;
}
private static int GetWordCharClass(char character)
{
if ((character >= 'a' && character <= 'z')
|| (character >= 'A' && character <= 'Z')
|| (character >= '0' && character <= '9')
|| character == '_')
{
return 1;
}
if (character >= 0x4E00 && character <= 0x9FFF)
{
return 2;
}
return 0;
}
private static DiffPosition BuildTextPosition(ParagraphState state, TextMatch match)
{
var startLeaf = state.Leaves.First(item => item.GlobalStart <= match.StartIndex && match.StartIndex < item.GlobalEnd);
var endIndexExclusive = match.StartIndex + match.Length;
var endLeaf = state.Leaves.First(item => item.GlobalStart < endIndexExclusive && endIndexExclusive <= item.GlobalEnd);
return new DiffPosition
{
StoryType = state.Target.StoryType,
ContainerPath = state.Target.ContainerPath,
ParagraphIndex = state.Target.ParagraphIndex,
ParagraphTextLength = -1,
ParagraphTextHash = null,
StartTextOffset = match.StartIndex,
EndTextOffset = endIndexExclusive,
StartRunIndex = startLeaf.RunIndex,
StartCharOffset = startLeaf.RunCharStart + (match.StartIndex - startLeaf.GlobalStart),
EndRunIndex = endLeaf.RunIndex,
EndCharOffset = endLeaf.RunCharStart + (endIndexExclusive - endLeaf.GlobalStart),
ObjectIndex = null,
};
}
private static string DetermineTextObjectType(ParagraphTarget target, ParagraphState state, TextMatch match, DiffPosition position)
{
if (string.Equals(position.StoryType, "chart", StringComparison.OrdinalIgnoreCase))
{
return "chartText";
}
var matchLeaves = state.Leaves
.Where(item => item.GlobalStart < match.StartIndex + match.Length && match.StartIndex < item.GlobalEnd)
.ToList();
if (matchLeaves.Any(item => item.IsChartText))
{
return "chartText";
}
if (string.Equals(target.StoryType, "textbox", StringComparison.OrdinalIgnoreCase))
{
return "textbox";
}
if (target.ParagraphElement.Ancestors<W.TableCell>().Any())
{
return "tableCell";
}
if (target.ParagraphElement.Descendants<W.Hyperlink>().Any())
{
return "hyperlinkDisplay";
}
if (matchLeaves.Any(IsFieldResultLeaf))
{
return "fieldResult";
}
if (matchLeaves.Any(item => item.IsEquationText))
{
return "equationText";
}
if (string.Equals(position.StoryType, "comment", StringComparison.OrdinalIgnoreCase))
{
return "comment";
}
if (string.Equals(position.StoryType, "footnote", StringComparison.OrdinalIgnoreCase))
{
return "footnote";
}
if (string.Equals(position.StoryType, "endnote", StringComparison.OrdinalIgnoreCase))
{
return "endnote";
}
return "paragraph";
}
private static bool IsFieldResultLeaf(TextLeaf leaf)
{
if (leaf == null || leaf.TextElement == null)
{
return false;
}
if (leaf.TextElement.Ancestors<W.SimpleField>().Any())
{
return true;
}
var run = leaf.Run as W.Run;
if (run == null)
{
return false;
}
return run.Descendants<W.FieldChar>().Any()
|| run.ElementsBefore().OfType<W.Run>().Any(item => item.Descendants<W.FieldChar>().Any(field =>
field.FieldCharType != null
&& field.FieldCharType.Value == W.FieldCharValues.Separate));
}
private static void RewriteRange(ParagraphState state, int globalStart, int length, string replacement, bool updateFont = false)
{
var globalEnd = globalStart + length;
var overlappingLeaves = state.Leaves
.Where(item => item.GlobalStart < globalEnd && globalStart < item.GlobalEnd)
.OrderBy(item => item.GlobalStart)
.ToList();
var remaining = replacement ?? string.Empty;
for (var index = 0; index < overlappingLeaves.Count; index++)
{
var leaf = overlappingLeaves[index];
var overlapStart = Math.Max(globalStart, leaf.GlobalStart);
var overlapEnd = Math.Min(globalEnd, leaf.GlobalEnd);
var localStart = overlapStart - leaf.GlobalStart;
var localEnd = overlapEnd - leaf.GlobalStart;
var originalSpanLength = localEnd - localStart;
string assigned;
if (index == overlappingLeaves.Count - 1)
{
assigned = remaining;
remaining = string.Empty;
}
else
{
var take = Math.Min(originalSpanLength, remaining.Length);
assigned = remaining.Substring(0, take);
remaining = remaining.Substring(take);
}
var updatedText = leaf.Text.Substring(0, localStart) + assigned + leaf.Text.Substring(localEnd);
leaf.Text = updatedText;
if (leaf.ApplyText != null)
{
leaf.ApplyText(updatedText);
}
else if (leaf.TextElement != null)
{
leaf.TextElement.Text = updatedText;
TrySetPreserveSpace(leaf.TextElement, updatedText);
}
if (updateFont && !string.IsNullOrEmpty(assigned))
{
EnsureRunFont(leaf.Run, assigned);
}
}
}
private static void EnsureRunFont(OpenXmlElement run, string text)
{
var wordRun = run as W.Run;
if (wordRun == null)
{
return;
}
var runProps = wordRun.RunProperties;
if (runProps == null)
{
runProps = new W.RunProperties();
wordRun.PrependChild(runProps);
}
var runFonts = runProps.RunFonts;
if (runFonts == null)
{
runFonts = new W.RunFonts();
runProps.PrependChild(runFonts);
}
var hasChinese = ContainsCjkCharacter(text);
var hasEnglish = ContainsLatinLetter(text);
if (hasChinese)
{
runFonts.EastAsia = "宋体";
runFonts.Ascii = "宋体";
runFonts.HighAnsi = "宋体";
}
else if (hasEnglish)
{
runFonts.Ascii = "Times New Roman";
runFonts.HighAnsi = "Times New Roman";
}
}
private static bool ContainsCjkCharacter(string text)
{
foreach (var c in text)
{
if ((c >= 0x4E00 && c <= 0x9FFF)
|| (c >= 0x3400 && c <= 0x4DBF)
|| (c >= 0xF900 && c <= 0xFAFF))
{
return true;
}
}
return false;
}
private static bool ContainsLatinLetter(string text)
{
foreach (var c in text)
{
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
{
return true;
}
}
return false;
}
private static void TrySetPreserveSpace(OpenXmlLeafTextElement textElement, string updatedText)
{
var property = textElement.GetType().GetProperty("Space");
if (property == null || !property.CanWrite)
{
return;
}
if (NeedsPreserve(updatedText))
{
property.SetValue(textElement, new EnumValue<SpaceProcessingModeValues>(SpaceProcessingModeValues.Preserve), null);
}
else
{
property.SetValue(textElement, null, null);
}
}
private static TextRange ResolveRange(ParagraphState state, DiffOperation operation, string paragraphKey)
{
var position = operation == null ? null : operation.Position;
var writtenText = operation == null ? string.Empty : (operation.WrittenText ?? string.Empty);
if (position == null)
{
throw new InvalidDataException("恢复失败:差异操作缺少位置信息。");
}
if (position.StartTextOffset >= 0)
{
var startIndexFromParagraph = position.StartTextOffset;
var writtenLengthFromParagraph = writtenText.Length;
if (startIndexFromParagraph <= state.Text.Length && startIndexFromParagraph + writtenLengthFromParagraph <= state.Text.Length)
{
return new TextRange
{
StartIndex = startIndexFromParagraph,
Length = writtenLengthFromParagraph,
};
}
if (position.EndTextOffset >= startIndexFromParagraph && position.EndTextOffset <= state.Text.Length)
{
return new TextRange
{
StartIndex = startIndexFromParagraph,
Length = position.EndTextOffset - startIndexFromParagraph,
};
}
}
var startLeaf = state.Leaves.FirstOrDefault(item =>
item.RunIndex == position.StartRunIndex
&& item.RunCharStart <= position.StartCharOffset
&& position.StartCharOffset <= item.RunCharEnd);
if (startLeaf == null)
{
throw new InvalidDataException("无法定位恢复起始位置。 " + BuildRestoreOperationContext(operation, paragraphKey, state));
}
var startIndex = startLeaf.GlobalStart + (position.StartCharOffset - startLeaf.RunCharStart);
var writtenLength = writtenText.Length;
if (startIndex + writtenLength <= state.Text.Length)
{
return new TextRange
{
StartIndex = startIndex,
Length = writtenLength,
};
}
var endLeaf = state.Leaves.FirstOrDefault(item =>
item.RunIndex == position.EndRunIndex
&& item.RunCharStart <= position.EndCharOffset
&& position.EndCharOffset <= item.RunCharEnd);
if (endLeaf == null)
{
throw new InvalidDataException("无法定位恢复结束位置。 " + BuildRestoreOperationContext(operation, paragraphKey, state));
}
var endIndex = endLeaf.GlobalStart + (position.EndCharOffset - endLeaf.RunCharStart);
if (endIndex < startIndex)
{
throw new InvalidDataException("恢复位置非法。 " + BuildRestoreOperationContext(operation, paragraphKey, state));
}
return new TextRange
{
StartIndex = startIndex,
Length = endIndex - startIndex,
};
}
private static string BuildParagraphLookupKey(string storyType, string containerPath, int paragraphIndex)
{
return (storyType ?? string.Empty) + "|" + (containerPath ?? string.Empty) + "|" + paragraphIndex;
}
private static string BuildRestoreOperationContext(DiffOperation operation, string paragraphKey, ParagraphState state)
{
var builder = new StringBuilder();
if (operation != null)
{
builder.Append("OperationId=").Append(operation.OperationId ?? string.Empty);
builder.Append("; Type=").Append(operation.Type ?? string.Empty);
builder.Append("; ObjectType=").Append(operation.ObjectType ?? string.Empty);
if (operation.Rule != null)
{
builder.Append("; RuleId=").Append(operation.Rule.Id ?? string.Empty);
builder.Append("; RuleName=").Append(operation.Rule.Name ?? string.Empty);
builder.Append("; Pattern=").Append(operation.Rule.Pattern ?? string.Empty);
}
builder.Append("; Position=").Append(FormatPosition(operation.Position));
builder.Append("; OriginalText=").Append(operation.OriginalText ?? string.Empty);
builder.Append("; WrittenText=").Append(operation.WrittenText ?? string.Empty);
}
builder.Append("; ParagraphKey=").Append(paragraphKey ?? string.Empty);
if (state != null)
{
builder.Append("; ParagraphLength=").Append(state.Text == null ? "0" : state.Text.Length.ToString(CultureInfo.InvariantCulture));
builder.Append("; ParagraphText=").Append(state.Text ?? string.Empty);
builder.Append("; ParagraphHash=").Append(BuildParagraphContentHash(state.Text));
builder.Append("; Leaves=").Append(FormatLeaves(state.Leaves));
}
return builder.ToString();
}
private static string FormatPosition(DiffPosition position)
{
if (position == null)
{
return "<null>";
}
return string.Format(
CultureInfo.InvariantCulture,
"story={0},container={1},paragraph={2},paragraphTextLength={3},paragraphTextHash={4},textStart={5},textEnd={6},runStart={7}:{8},runEnd={9}:{10},objectIndex={11}",
position.StoryType ?? string.Empty,
position.ContainerPath ?? string.Empty,
position.ParagraphIndex,
position.ParagraphTextLength,
position.ParagraphTextHash ?? "<null>",
position.StartTextOffset,
position.EndTextOffset,
position.StartRunIndex,
position.StartCharOffset,
position.EndRunIndex,
position.EndCharOffset,
position.ObjectIndex.HasValue ? position.ObjectIndex.Value.ToString(CultureInfo.InvariantCulture) : "<null>");
}
private static string BuildParagraphScopeKey(string storyType, string containerPath)
{
return (storyType ?? string.Empty) + "|" + (containerPath ?? string.Empty);
}
private static string BuildParagraphContentHash(string text)
{
return HashUtility.ComputeSha256Base64(text ?? string.Empty);
}
private static bool TryResolveRestoreTargetFallback(
IDictionary<string, List<ParagraphTarget>> paragraphScopeLookup,
string scopeKey,
DiffOperation operation,
out ParagraphTarget target,
out ParagraphState state,
out TextRange range,
out string actualWritten,
out bool ambiguous)
{
target = null;
state = null;
range = null;
actualWritten = null;
ambiguous = false;
if (paragraphScopeLookup == null || string.IsNullOrEmpty(scopeKey) || !paragraphScopeLookup.TryGetValue(scopeKey, out var candidates))
{
return false;
}
var resolvedCandidates = new List<ResolvedParagraphCandidate>();
foreach (var candidate in candidates)
{
var candidateState = BuildParagraphState(candidate);
var strictMatch = TryResolveOperationInParagraph(candidateState, operation, out var candidateRange, out var candidateWritten);
var startOffsetDistance = 0;
if (!strictMatch
&& !TryResolveOperationByWrittenText(candidateState, operation, out candidateRange, out candidateWritten, out startOffsetDistance))
{
continue;
}
resolvedCandidates.Add(new ResolvedParagraphCandidate
{
Target = candidate,
State = candidateState,
Range = candidateRange,
ActualWritten = candidateWritten,
IsStrictMatch = strictMatch,
BoundaryPenalty = ComputeRangeBoundaryPenalty(candidateState, candidateRange),
StartOffsetDistance = strictMatch
? ComputeStartOffsetDistance(operation == null ? null : operation.Position, candidateRange)
: startOffsetDistance,
});
}
if (resolvedCandidates.Count == 0)
{
return false;
}
var filteredCandidates = resolvedCandidates;
if (filteredCandidates.Any(item => item.IsStrictMatch))
{
filteredCandidates = filteredCandidates
.Where(item => item.IsStrictMatch)
.ToList();
}
if (filteredCandidates.Count > 1)
{
var bestBoundaryPenalty = filteredCandidates.Min(item => item.BoundaryPenalty);
filteredCandidates = filteredCandidates
.Where(item => item.BoundaryPenalty == bestBoundaryPenalty)
.ToList();
}
var position = operation == null ? null : operation.Position;
if (position != null && position.ParagraphIndex >= 0 && filteredCandidates.Count > 1)
{
var bestDistance = filteredCandidates
.Min(item => Math.Abs(item.Target.ParagraphIndex - position.ParagraphIndex));
filteredCandidates = filteredCandidates
.Where(item => Math.Abs(item.Target.ParagraphIndex - position.ParagraphIndex) == bestDistance)
.ToList();
}
if (position != null && position.StartTextOffset >= 0 && filteredCandidates.Count > 1)
{
var bestOffsetDistance = filteredCandidates.Min(item => item.StartOffsetDistance);
filteredCandidates = filteredCandidates
.Where(item => item.StartOffsetDistance == bestOffsetDistance)
.ToList();
}
if (filteredCandidates.Count != 1)
{
ambiguous = true;
return false;
}
var resolved = filteredCandidates[0];
target = resolved.Target;
state = resolved.State;
range = resolved.Range;
actualWritten = resolved.ActualWritten;
return true;
}
private static bool TryResolveOperationInParagraph(ParagraphState state, DiffOperation operation, out TextRange range, out string actualWritten)
{
range = null;
actualWritten = null;
if (state == null || !TryResolveRange(state, operation, out range))
{
return false;
}
actualWritten = ExtractTextRange(state, range);
if (!string.Equals(actualWritten, operation == null ? string.Empty : (operation.WrittenText ?? string.Empty), StringComparison.Ordinal))
{
return false;
}
return MatchesParagraphLength(operation == null ? null : operation.Position, state);
}
private static bool MatchesParagraphLength(DiffPosition position, ParagraphState state)
{
if (position == null || state == null)
{
return true;
}
if (position.ParagraphTextLength < 0)
{
return true;
}
var currentLength = (state.Text ?? string.Empty).Length;
return position.ParagraphTextLength == currentLength;
}
private static bool TryResolveOperationByWrittenText(
ParagraphState state,
DiffOperation operation,
out TextRange range,
out string actualWritten,
out int startOffsetDistance)
{
range = null;
actualWritten = null;
startOffsetDistance = int.MaxValue;
if (state == null || operation == null)
{
return false;
}
var writtenText = operation.WrittenText ?? string.Empty;
if (string.IsNullOrEmpty(writtenText) || !MatchesParagraphIdentity(operation.Position, state))
{
return false;
}
var bestIndex = -1;
var bestBoundaryPenalty = int.MaxValue;
var searchStart = 0;
while (searchStart <= state.Text.Length - writtenText.Length)
{
var matchIndex = state.Text.IndexOf(writtenText, searchStart, StringComparison.Ordinal);
if (matchIndex < 0)
{
break;
}
var candidateRange = new TextRange
{
StartIndex = matchIndex,
Length = writtenText.Length,
};
var currentBoundaryPenalty = ComputeRangeBoundaryPenalty(state, candidateRange);
var currentDistance = ComputeStartOffsetDistance(operation.Position, candidateRange);
if (bestIndex < 0
|| currentBoundaryPenalty < bestBoundaryPenalty
|| (currentBoundaryPenalty == bestBoundaryPenalty && currentDistance < startOffsetDistance))
{
bestIndex = matchIndex;
bestBoundaryPenalty = currentBoundaryPenalty;
startOffsetDistance = currentDistance;
}
searchStart = matchIndex + Math.Max(writtenText.Length, 1);
}
if (bestIndex < 0)
{
return false;
}
range = new TextRange
{
StartIndex = bestIndex,
Length = writtenText.Length,
};
actualWritten = writtenText;
return true;
}
private static bool MatchesParagraphIdentity(DiffPosition position, ParagraphState state)
{
if (position == null || state == null)
{
return true;
}
var text = state.Text ?? string.Empty;
if (position.ParagraphTextLength >= 0 && position.ParagraphTextLength != text.Length)
{
return false;
}
if (!string.IsNullOrEmpty(position.ParagraphTextHash)
&& !string.Equals(position.ParagraphTextHash, BuildParagraphContentHash(text), StringComparison.Ordinal))
{
return false;
}
return true;
}
private static bool TryResolveRange(ParagraphState state, DiffOperation operation, out TextRange range)
{
range = null;
var position = operation == null ? null : operation.Position;
var writtenText = operation == null ? string.Empty : (operation.WrittenText ?? string.Empty);
if (state == null || position == null)
{
return false;
}
if (position.StartTextOffset >= 0)
{
var startIndexFromParagraph = position.StartTextOffset;
var writtenLengthFromParagraph = writtenText.Length;
if (startIndexFromParagraph <= state.Text.Length && startIndexFromParagraph + writtenLengthFromParagraph <= state.Text.Length)
{
range = new TextRange
{
StartIndex = startIndexFromParagraph,
Length = writtenLengthFromParagraph,
};
return true;
}
if (position.EndTextOffset >= startIndexFromParagraph && position.EndTextOffset <= state.Text.Length)
{
range = new TextRange
{
StartIndex = startIndexFromParagraph,
Length = position.EndTextOffset - startIndexFromParagraph,
};
return true;
}
}
var startLeaf = state.Leaves.FirstOrDefault(item =>
item.RunIndex == position.StartRunIndex
&& item.RunCharStart <= position.StartCharOffset
&& position.StartCharOffset <= item.RunCharEnd);
if (startLeaf == null)
{
return false;
}
var startIndex = startLeaf.GlobalStart + (position.StartCharOffset - startLeaf.RunCharStart);
var writtenLength = writtenText.Length;
if (startIndex + writtenLength <= state.Text.Length)
{
range = new TextRange
{
StartIndex = startIndex,
Length = writtenLength,
};
return true;
}
var endLeaf = state.Leaves.FirstOrDefault(item =>
item.RunIndex == position.EndRunIndex
&& item.RunCharStart <= position.EndCharOffset
&& position.EndCharOffset <= item.RunCharEnd);
if (endLeaf == null)
{
return false;
}
var endIndex = endLeaf.GlobalStart + (position.EndCharOffset - endLeaf.RunCharStart);
if (endIndex < startIndex)
{
return false;
}
range = new TextRange
{
StartIndex = startIndex,
Length = endIndex - startIndex,
};
return true;
}
private static TextRange TryCreateDiagnosticRange(ParagraphState state, DiffOperation operation)
{
return TryResolveRange(state, operation, out var range) ? range : null;
}
private static int ComputeStartOffsetDistance(DiffPosition position, TextRange range)
{
if (position == null || range == null || position.StartTextOffset < 0)
{
return 0;
}
return Math.Abs(range.StartIndex - position.StartTextOffset);
}
private static bool ShouldTrustExactRestoreCandidate(DiffOperation operation, ParagraphState state, TextRange range)
{
if (operation == null)
{
return true;
}
if (HasStrongParagraphIdentity(operation.Position))
{
return true;
}
return ComputeRangeBoundaryPenalty(state, range) == 0;
}
private static bool HasStrongParagraphIdentity(DiffPosition position)
{
return position != null
&& (position.ParagraphTextLength >= 0 || !string.IsNullOrEmpty(position.ParagraphTextHash));
}
private static int ComputeRangeBoundaryPenalty(ParagraphState state, TextRange range)
{
if (state == null || string.IsNullOrEmpty(state.Text) || range == null || range.Length <= 0)
{
return 0;
}
if (range.StartIndex < 0 || range.StartIndex + range.Length > state.Text.Length)
{
return int.MaxValue;
}
var penalty = 0;
var matchedText = ExtractTextRange(state, range);
if (string.IsNullOrEmpty(matchedText))
{
return 0;
}
if (range.StartIndex > 0 && state.Text[range.StartIndex - 1] == matchedText[0])
{
penalty++;
}
var endIndex = range.StartIndex + range.Length;
if (endIndex < state.Text.Length && state.Text[endIndex] == matchedText[matchedText.Length - 1])
{
penalty++;
}
return penalty;
}
private static string ExtractTextRange(ParagraphState state, TextRange range)
{
if (state == null || string.IsNullOrEmpty(state.Text) || range == null)
{
return string.Empty;
}
if (range.StartIndex < 0 || range.Length < 0 || range.StartIndex + range.Length > state.Text.Length)
{
return string.Empty;
}
return range.Length == 0 ? string.Empty : state.Text.Substring(range.StartIndex, range.Length);
}
private static string FormatLeaves(IEnumerable<TextLeaf> leaves)
{
if (leaves == null)
{
return "<null>";
}
return string.Join(
" | ",
leaves.Select(item => string.Format(
CultureInfo.InvariantCulture,
"run={0},runRange=[{1},{2}],globalRange=[{3},{4}],text={5}",
item == null ? -1 : item.RunIndex,
item == null ? -1 : item.RunCharStart,
item == null ? -1 : item.RunCharEnd,
item == null ? -1 : item.GlobalStart,
item == null ? -1 : item.GlobalEnd,
item == null ? string.Empty : item.Text ?? string.Empty)));
}
private static bool NeedsPreserve(string value)
{
return !string.IsNullOrEmpty(value)
&& (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[value.Length - 1]) || value.Contains(" "));
}
private sealed class ProcessedImageResult
{
public string SourceKind { get; set; }
public DiffImageData OriginalImage { get; set; }
public DiffImageData ConvertedBitmapImage { get; set; }
public DiffImageData NewImage { get; set; }
public double ModifiedPixelRatio { get; set; }
public int Seed { get; set; }
}
private sealed class NoiseCell
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int Area { get; set; }
public int Quota { get; set; }
public double Fraction { get; set; }
public double TieBreaker { get; set; }
}
private class PartContext
{
public OpenXmlPart Part { get; set; }
public OpenXmlPartRootElement Root { get; set; }
public string PartUri { get; set; }
public string StoryType { get; set; }
}
private class ParagraphTarget
{
public PartContext PartContext { get; set; }
public OpenXmlElement ParagraphElement { get; set; }
public string StoryType { get; set; }
public string ContainerPath { get; set; }
public int ParagraphIndex { get; set; }
}
private class ParagraphState
{
public ParagraphTarget Target { get; set; }
public string Text { get; set; }
public List<TextLeaf> Leaves { get; set; }
public ISet<int> LogicalWordBoundaries { get; set; }
}
private class TextLeaf
{
public OpenXmlLeafTextElement TextElement { get; set; }
public OpenXmlElement Run { get; set; }
public int RunIndex { get; set; }
public int RunCharStart { get; set; }
public int RunCharEnd
{
get { return RunCharStart + (Text == null ? 0 : Text.Length); }
}
public int GlobalStart { get; set; }
public int GlobalEnd
{
get { return GlobalStart + (Text == null ? 0 : Text.Length); }
}
public string Text { get; set; }
public bool IsEquationText { get; set; }
public bool IsChartText { get; set; }
public Action<string> ApplyText { get; set; }
}
private class TextMatch
{
public int StartIndex { get; set; }
public int Length { get; set; }
public string OriginalText { get; set; }
public string ReplacementTemplateResult { get; set; }
}
private class TextRange
{
public int StartIndex { get; set; }
public int Length { get; set; }
}
private class ResolvedParagraphCandidate
{
public ParagraphTarget Target { get; set; }
public ParagraphState State { get; set; }
public TextRange Range { get; set; }
public string ActualWritten { get; set; }
public bool IsStrictMatch { get; set; }
public int BoundaryPenalty { get; set; }
public int StartOffsetDistance { get; set; }
}
private class ImageContext
{
public OpenXmlPart ParentPart { get; set; }
public ImagePart ImagePart { get; set; }
public string RelationshipId { get; set; }
public string RelationKey { get; set; }
public string PartUri { get; set; }
public string StoryType { get; set; }
public string ContainerPath { get; set; }
public string ContainerKind { get; set; }
public string EmbeddedProgramId { get; set; }
public string EmbeddedPackageExtension { get; set; }
public string ObjectType { get; set; }
public DiffImageLayout Layout { get; set; }
public int ObjectIndex { get; set; }
}
private class ImageRestoreCandidate
{
public ImageContext Image { get; set; }
public bool IsProcessed { get; set; }
public bool StoryMatches { get; set; }
public bool ContainerKindMatches { get; set; }
public bool EmbeddedProgramMatches { get; set; }
public bool EmbeddedPackageMatches { get; set; }
public bool ObjectTypeMatches { get; set; }
public bool ObjectTypeCompatible { get; set; }
public bool LayoutMatches { get; set; }
public bool ContentMatches { get; set; }
public bool SkipContentVerification { get; set; }
public int PathSimilarity { get; set; }
public int ObjectIndexDistance { get; set; }
}
private sealed class ReplacementOutputPaths
{
public string OutputPath { get; set; }
public string DiffPath { get; set; }
public string BmpPath { get; set; }
}
private sealed class BrokenFieldFinding
{
public string StoryType { get; set; }
public string ContainerPath { get; set; }
public int ParagraphIndex { get; set; }
public string Marker { get; set; }
public string Excerpt { get; set; }
public string FullParagraphText { get; set; }
public int? TableIndex { get; set; }
public int? RowIndex { get; set; }
public int? ColumnIndex { get; set; }
public string PreviousParagraphText { get; set; }
public string NextParagraphText { get; set; }
public string SameRowCellsText { get; set; }
public string SameColumnCellsText { get; set; }
public string Identity =>
string.Format(
CultureInfo.InvariantCulture,
"{0}|{1}|{2}|{3}",
StoryType ?? string.Empty,
ContainerPath ?? string.Empty,
ParagraphIndex,
Marker ?? string.Empty);
}
}
}