commit 74e4738cfc1fa415d9016f1ed21f0e09465bda7c Author: lihansani Date: Mon Jul 13 09:39:56 2026 +0800 first commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a1dc8ea --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build *)", + "Bash(cmd //c dir /b/s \"e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本\\\\net6-0707\\\\tools\\\\DocTo\\\\\" 2>&1 | head -20)", + "Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本\\\\net6-0707\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length\")", + "Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本' | Select-Object Name\")", + "Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\历史版本\\\\net6-0707' -Directory | Select-Object Name\")", + "Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\打包\\\\Word文档\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length,LastWriteTime\")", + "Bash(powershell -NoProfile -Command \"Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\src\\\\DCIT.App\\\\bin\\\\Release\\\\net6.0-windows\\\\publish\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length,LastWriteTime\")", + "Bash(powershell -NoProfile -Command \"New-Item -ItemType Directory -Force -Path 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\tools\\\\DocTo' | Out-Null; Copy-Item -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\打包\\\\Word文档\\\\tools\\\\DocTo\\\\docto.exe' -Destination 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\tools\\\\DocTo\\\\docto.exe' -Force; Get-ChildItem -LiteralPath 'e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\tools\\\\DocTo\\\\' | Select-Object Name,Length\")", + "Bash(dotnet publish *)", + "Bash(powershell -NoProfile -ExecutionPolicy Bypass -File \"e:\\\\projects\\\\wxwx\\\\projects_job\\\\GeneralRequirement\\\\WORD文档替换与还原工具\\\\codex\\\\DCIT\\\\src\\\\_list_publish.ps1\")" + ] + } +} diff --git a/DCIT.App b/DCIT.App new file mode 160000 index 0000000..f8f127a --- /dev/null +++ b/DCIT.App @@ -0,0 +1 @@ +Subproject commit f8f127a2c77c35f8ea2d3129c548bdc1ac0c789c diff --git a/DCIT.Core/DCIT.Core.csproj b/DCIT.Core/DCIT.Core.csproj new file mode 100644 index 0000000..d67beb4 --- /dev/null +++ b/DCIT.Core/DCIT.Core.csproj @@ -0,0 +1,14 @@ + + + net6.0-windows + disable + disable + latest + DCIT.Core + + + + + + + \ No newline at end of file diff --git a/DCIT.Core/Models/AppConfig.cs b/DCIT.Core/Models/AppConfig.cs new file mode 100644 index 0000000..486ebd0 --- /dev/null +++ b/DCIT.Core/Models/AppConfig.cs @@ -0,0 +1,98 @@ +using System; + +namespace DCIT.Core.Models +{ + public class AppConfig + { + public string Format { get; set; } = "dcit.config"; + + public string Version { get; set; } = "1.0"; + + public DateTime SavedAt { get; set; } = DateTime.Now; + + public AppSettings Settings { get; set; } = new AppSettings(); + + public ReplacePageState ReplacePage { get; set; } = new ReplacePageState(); + + public RestorePageState RestorePage { get; set; } = new RestorePageState(); + } + + public class AppSettings + { + public bool JsonDiffEncryption { get; set; } = true; + + public bool EnableImageReplacement { get; set; } = true; + + public bool UseFixedImageSeed { get; set; } + + public int FixedImageSeed { get; set; } = 20260424; + + public AutoExtractSettings AutoExtract { get; set; } = new AutoExtractSettings(); + } + + public class AutoExtractSettings + { + public bool ExtractKeywords { get; set; } = true; + + public bool ExtractHighFrequencyWords { get; set; } = true; + + public bool ExtractHighFrequencySentences { get; set; } = true; + + public bool UseKeywordFrequency { get; set; } = true; + + public bool UseKeywordTfIdf { get; set; } = true; + + public bool UseKeywordTextRank { get; set; } = true; + + public int MaxKeywordCount { get; set; } = 10; + + public int MaxWordCount { get; set; } = 10; + + public int MaxSentenceCount { get; set; } = 10; + + public bool CountChineseWords { get; set; } = true; + + public bool CountEnglishWords { get; set; } = true; + + public bool CountChineseSentences { get; set; } = true; + + public bool CountEnglishSentences { get; set; } = true; + + public int MinChineseWordLength { get; set; } = 1; + + public int MinEnglishWordLength { get; set; } = 1; + + public string SentenceDelimiters { get; set; } = "。!?;.!?;\r\n"; + + public bool ExcludeEmailAddresses { get; set; } + } + + public class ReplacePageState + { + public string SourceDirectory { get; set; } = string.Empty; + + public string OutputDirectory { get; set; } = string.Empty; + + public string RuleFilePath { get; set; } = string.Empty; + } + + public class RestorePageState + { + public string ReplaceDirectory { get; set; } = string.Empty; + + public string RestoreDirectory { get; set; } = string.Empty; + + public string DiffInputFormat { get; set; } = ".diff"; + } + + public class ConfigurationLoadResult + { + public AppConfig Config { get; set; } = new AppConfig(); + + public bool IsMissing { get; set; } + + public bool IsCorrupted { get; set; } + + public string ErrorMessage { get; set; } + } +} \ No newline at end of file diff --git a/DCIT.Core/Models/AutoExtractModels.cs b/DCIT.Core/Models/AutoExtractModels.cs new file mode 100644 index 0000000..2d747d0 --- /dev/null +++ b/DCIT.Core/Models/AutoExtractModels.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DCIT.Core.Models +{ + public class ExtractionProgress + { + public int Current { get; set; } + + public int Total { get; set; } + + public string Message { get; set; } + } + + public class ExtractedDocumentText + { + public string FilePath { get; set; } + + public string VisibleText { get; set; } + } + + public class ExtractionCandidate : INotifyPropertyChanged + { + private bool _append = true; + private AutoExtractCategory _primaryCategory; + private string _categoryDisplay; + private string _originalText; + private string _statisticDisplay; + private string _firstDocumentPath; + private int _firstOccurrenceOrder; + + public bool Append + { + get { return _append; } + set { SetField(ref _append, value); } + } + + public AutoExtractCategory PrimaryCategory + { + get { return _primaryCategory; } + set { SetField(ref _primaryCategory, value); } + } + + public string CategoryDisplay + { + get { return _categoryDisplay; } + set { SetField(ref _categoryDisplay, value); } + } + + public string OriginalText + { + get { return _originalText; } + set { SetField(ref _originalText, value); } + } + + public string StatisticDisplay + { + get { return _statisticDisplay; } + set { SetField(ref _statisticDisplay, value); } + } + + public string FirstDocumentPath + { + get { return _firstDocumentPath; } + set { SetField(ref _firstDocumentPath, value); } + } + + public int FirstOccurrenceOrder + { + get { return _firstOccurrenceOrder; } + set { SetField(ref _firstOccurrenceOrder, value); } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected void SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return; + } + + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public class AutoExtractResult + { + public List Candidates { get; } = new List(); + + public List Warnings { get; } = new List(); + + public int DocumentsProcessed { get; set; } + + public int DocumentsSkipped { get; set; } + } +} diff --git a/DCIT.Core/Models/BatchExecutionModels.cs b/DCIT.Core/Models/BatchExecutionModels.cs new file mode 100644 index 0000000..788b23e --- /dev/null +++ b/DCIT.Core/Models/BatchExecutionModels.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace DCIT.Core.Models +{ + public enum BatchExecutionSkipReason + { + DuplicateSourcePath = 0, + DuplicateOutputPath = 1, + DuplicateDiffPath = 2 + } + + public sealed class BatchExecutionSkipItem + { + public FileScanItem Item { get; set; } + + public BatchExecutionSkipReason Reason { get; set; } + + public string ConflictingPath { get; set; } + + public string ExistingPath { get; set; } + + public string Message + { + get + { + switch (Reason) + { + case BatchExecutionSkipReason.DuplicateSourcePath: + return "源文件与批次中已有任务重复,已跳过。"; + case BatchExecutionSkipReason.DuplicateOutputPath: + return "输出目标与批次中已有任务冲突,已跳过。"; + case BatchExecutionSkipReason.DuplicateDiffPath: + return "差异文件目标与批次中已有任务冲突,已跳过。"; + default: + return "批处理任务冲突,已跳过。"; + } + } + } + } + + public sealed class BatchExecutionPlan + { + public IList ExecutionItems { get; set; } = new List(); + + public IList SkippedItems { get; set; } = new List(); + + public int DegreeOfParallelism { get; set; } = 1; + + public bool RequiresSerialExecution { get; set; } + } +} diff --git a/DCIT.Core/Models/DiffModels.cs b/DCIT.Core/Models/DiffModels.cs new file mode 100644 index 0000000..e3c77f8 --- /dev/null +++ b/DCIT.Core/Models/DiffModels.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; + +namespace DCIT.Core.Models +{ + public class DiffDocument + { + public string Format { get; set; } = "dcit-diff"; + + public string Version { get; set; } = "1.0"; + + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.Now; + + public bool Encrypted { get; set; } + + public DiffAppInfo App { get; set; } = new DiffAppInfo(); + + public DiffAlgorithmInfo Algorithm { get; set; } = new DiffAlgorithmInfo(); + + public DiffDocumentInfo SourceDocument { get; set; } = new DiffDocumentInfo(); + + public DiffDocumentInfo ReplacedDocument { get; set; } = new DiffDocumentInfo(); + + public DiffRuleFileInfo RuleFile { get; set; } = new DiffRuleFileInfo(); + + public DiffIntegrityInfo Integrity { get; set; } = new DiffIntegrityInfo(); + + public DiffEncryptionInfo Encryption { get; set; } + + public string PayloadCiphertext { get; set; } + + public DiffPayload Payload { get; set; } + } + + public class DiffAppInfo + { + public string Name { get; set; } = "DCIT"; + + public string Version { get; set; } = "1.0.0"; + } + + public class DiffAlgorithmInfo + { + public string DiffVersion { get; set; } = "1.0"; + + public string BmpCodecVersion { get; set; } = "1.0"; + } + + public class DiffDocumentInfo + { + public string FileName { get; set; } + + public string RelativePath { get; set; } + + public string Extension { get; set; } + + public string FingerprintAlgorithm { get; set; } = "SHA-256"; + + public string Fingerprint { get; set; } + } + + public class DiffRuleFileInfo + { + public string FileName { get; set; } + + public string Version { get; set; } = "1.0"; + + public string FingerprintAlgorithm { get; set; } = "SHA-256"; + + public string Fingerprint { get; set; } + } + + public class DiffIntegrityInfo + { + public string HashAlgorithm { get; set; } = "SHA-256"; + + public string PayloadHash { get; set; } + + public string TamperProtectionAlgorithm { get; set; } = "HMAC-SHA256"; + + public string TamperProtectionValue { get; set; } + } + + public class DiffEncryptionInfo + { + public string Algorithm { get; set; } = "AES-256-CBC+HMAC-SHA256"; + + public string KeyId { get; set; } = "default"; + + public string Nonce { get; set; } + + public string Tag { get; set; } + } + + public class DiffPayload + { + public DiffStatistics Statistics { get; set; } = new DiffStatistics(); + + public List Operations { get; set; } = new List(); + } + + public class DiffStatistics + { + public int TextReplaceCount { get; set; } + + public int ImageReplaceCount { get; set; } + } + + public class DiffOperation + { + public string OperationId { get; set; } + + public string Type { get; set; } + + public string ObjectType { get; set; } + + public string ContainerKind { get; set; } + + public string EmbeddedProgramId { get; set; } + + public string EmbeddedPackageExtension { get; set; } + + public DiffPosition Position { get; set; } = new DiffPosition(); + + public DiffRuleHitInfo Rule { get; set; } + + public string OriginalText { get; set; } + + public string ReplacementTemplateResult { get; set; } + + public string WrittenText { get; set; } + + public int? DisplayWidthOriginal { get; set; } + + public int? DisplayWidthWritten { get; set; } + + public bool? Truncated { get; set; } + + public bool? PaddingApplied { get; set; } + + public string SourceKind { get; set; } + + public DiffImageData OriginalImage { get; set; } + + public DiffImageData ConvertedBitmapImage { get; set; } + + public DiffImageData NewImage { get; set; } + + public int? Seed { get; set; } + + public DiffNoiseSummary NoiseSummary { get; set; } + + public DiffImageLayout Layout { get; set; } + } + + public class DiffPosition + { + public string StoryType { get; set; } + + public string ContainerPath { get; set; } + + public int ParagraphIndex { get; set; } + + public int ParagraphTextLength { get; set; } = -1; + + public string ParagraphTextHash { get; set; } + + public int StartTextOffset { get; set; } = -1; + + public int EndTextOffset { get; set; } = -1; + + public int StartRunIndex { get; set; } + + public int StartCharOffset { get; set; } + + public int EndRunIndex { get; set; } + + public int EndCharOffset { get; set; } + + public int? ObjectIndex { get; set; } + } + + public class DiffRuleHitInfo + { + public string Id { get; set; } + + public string Name { get; set; } + + public string MatchMode { get; set; } + + public string Pattern { get; set; } + + public int HitIndex { get; set; } + } + + public class DiffImageData + { + public string Format { get; set; } + + public int WidthPx { get; set; } + + public int HeightPx { get; set; } + + public string Data { get; set; } + } + + public class DiffNoiseSummary + { + public double ModifiedPixelRatio { get; set; } + } + + public class DiffImageLayout + { + public long DisplayWidthEmu { get; set; } + + public long DisplayHeightEmu { get; set; } + + public string WrapMode { get; set; } + + public double RotationDegree { get; set; } + } +} diff --git a/DCIT.Core/Models/Enums.cs b/DCIT.Core/Models/Enums.cs new file mode 100644 index 0000000..4607479 --- /dev/null +++ b/DCIT.Core/Models/Enums.cs @@ -0,0 +1,53 @@ +using System; + +namespace DCIT.Core.Models +{ + public enum RuleMatchMode + { + PlainText = 0, + RegularExpression = 1 + } + + public enum ProcessingStatus + { + Pending = 0, + InProgress = 1, + Success = 2, + Failed = 3, + Stopped = 4 + } + + public enum LogLevel + { + Info = 0, + Warning = 1, + Error = 2 + } + + public enum AutoExtractCategory + { + Keyword = 0, + HighFrequencyWord = 1, + HighFrequencySentence = 2 + } + + public enum KeywordAlgorithm + { + Frequency = 0, + TfIdf = 1, + TextRank = 2 + } + + public enum RuleValidationSeverity + { + Info = 0, + Warning = 1, + Error = 2 + } + + public enum RuleTarget + { + DocumentContent = 0, + FileName = 1 + } +} diff --git a/DCIT.Core/Models/FileScanItem.cs b/DCIT.Core/Models/FileScanItem.cs new file mode 100644 index 0000000..3600c09 --- /dev/null +++ b/DCIT.Core/Models/FileScanItem.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DCIT.Core.Models +{ + public class FileScanItem : INotifyPropertyChanged + { + private bool _isSelected = true; + private string _sourcePath; + private string _outputPath; + private string _diffPath; + private int _textOperationCount; + private int _imageOperationCount; + private ProcessingStatus _status = ProcessingStatus.Pending; + + public bool IsSelected + { + get { return _isSelected; } + set { SetField(ref _isSelected, value); } + } + + public string SourcePath + { + get { return _sourcePath; } + set { SetField(ref _sourcePath, value); } + } + + public string OutputPath + { + get { return _outputPath; } + set { SetField(ref _outputPath, value); } + } + + public string DiffPath + { + get { return _diffPath; } + set { SetField(ref _diffPath, value); } + } + + public int TextOperationCount + { + get { return _textOperationCount; } + set { SetField(ref _textOperationCount, value); } + } + + public int ImageOperationCount + { + get { return _imageOperationCount; } + set { SetField(ref _imageOperationCount, value); } + } + + public ProcessingStatus Status + { + get { return _status; } + set { SetField(ref _status, value); } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected void SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return; + } + + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/DCIT.Core/Models/LogEntry.cs b/DCIT.Core/Models/LogEntry.cs new file mode 100644 index 0000000..fac6a22 --- /dev/null +++ b/DCIT.Core/Models/LogEntry.cs @@ -0,0 +1,17 @@ +using System; + +namespace DCIT.Core.Models +{ + public class LogEntry + { + public DateTime Timestamp { get; set; } = DateTime.Now; + + public LogLevel Level { get; set; } + + public string EventCode { get; set; } + + public string Message { get; set; } + + public string Detail { get; set; } + } +} diff --git a/DCIT.Core/Models/LogWriteFailureEventArgs.cs b/DCIT.Core/Models/LogWriteFailureEventArgs.cs new file mode 100644 index 0000000..9261dd2 --- /dev/null +++ b/DCIT.Core/Models/LogWriteFailureEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace DCIT.Core.Models +{ + public sealed class LogWriteFailureEventArgs : EventArgs + { + public LogWriteFailureEventArgs(LogEntry entry, string logFilePath, Exception exception) + { + Entry = entry; + LogFilePath = logFilePath; + Exception = exception; + } + + public LogEntry Entry { get; private set; } + + public string LogFilePath { get; private set; } + + public Exception Exception { get; private set; } + } +} diff --git a/DCIT.Core/Models/ProcessingModels.cs b/DCIT.Core/Models/ProcessingModels.cs new file mode 100644 index 0000000..1bd1bd9 --- /dev/null +++ b/DCIT.Core/Models/ProcessingModels.cs @@ -0,0 +1,72 @@ +using System; + +namespace DCIT.Core.Models +{ + public class ReplaceFileRequest + { + public string SourcePath { get; set; } + + public string OutputPath { get; set; } + + public string DiffPath { get; set; } + + public string SourceRootDirectory { get; set; } + + public string RuleFilePath { get; set; } + + public RuleFile RuleFile { get; set; } + + public bool EncryptDiff { get; set; } + + public bool EnableImageReplacement { get; set; } = true; + + public int? FixedImageSeed { get; set; } + } + + public class RestoreFileRequest + { + public string ReplacedPath { get; set; } + + public string DiffPath { get; set; } + + public string OutputPath { get; set; } + + public string ReplaceRootDirectory { get; set; } + + public string CurrentRuleFilePath { get; set; } + } + + public class FileProcessProgress + { + public string FilePath { get; set; } + + public int FilePercent { get; set; } + + public string CurrentRuleId { get; set; } + + public string CurrentRuleName { get; set; } + + public int CurrentFileTextCount { get; set; } + + public int TotalTextCount { get; set; } + + public string Message { get; set; } + + public bool IsWarning { get; set; } + } + + public class FileProcessResult + { + public string OutputPath { get; set; } + + public string DiffPath { get; set; } + + public string BmpPath { get; set; } + + public int TextOperationCount { get; set; } + + public int ImageOperationCount { get; set; } + + public bool Stopped { get; set; } + } +} \ No newline at end of file diff --git a/DCIT.Core/Models/RuleDefinition.cs b/DCIT.Core/Models/RuleDefinition.cs new file mode 100644 index 0000000..8aa45c8 --- /dev/null +++ b/DCIT.Core/Models/RuleDefinition.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace DCIT.Core.Models +{ + public class RuleDefinition : INotifyPropertyChanged + { + private string _id; + private string _name; + private bool _isEnabled = true; + private RuleMatchMode _matchMode = RuleMatchMode.PlainText; + private bool _isCaseSensitive; + private bool _isWholeWord; + private RuleTarget _target = RuleTarget.DocumentContent; + private string _searchText; + private string _replacementText; + private string _note; + + public string Id + { + get { return _id; } + set { SetField(ref _id, value); } + } + + public string Name + { + get { return _name; } + set { SetField(ref _name, value); } + } + + public bool IsEnabled + { + get { return _isEnabled; } + set { SetField(ref _isEnabled, value); } + } + + public RuleMatchMode MatchMode + { + get { return _matchMode; } + set { SetField(ref _matchMode, value); } + } + + public RuleTarget Target + { + get { return _target; } + set { SetField(ref _target, value); } + } + + public bool IsCaseSensitive + { + get { return _isCaseSensitive; } + set { SetField(ref _isCaseSensitive, value); } + } + + public bool IsWholeWord + { + get { return _isWholeWord; } + set { SetField(ref _isWholeWord, value); } + } + + public string SearchText + { + get { return _searchText; } + set { SetField(ref _searchText, value); } + } + + public string ReplacementText + { + get { return _replacementText; } + set { SetField(ref _replacementText, value); } + } + + public string Note + { + get { return _note; } + set { SetField(ref _note, value); } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected void SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return; + } + + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public class RuleFile + { + public string Format { get; set; } = "dcit.rule"; + + public string Version { get; set; } = "1.0"; + + public List Rules { get; set; } = new List(); + } + + public class RuleValidationIssue + { + public RuleValidationSeverity Severity { get; set; } + + public int Index { get; set; } + + public string RuleId { get; set; } + + public string RuleName { get; set; } + + public string Message { get; set; } + } + + public class RuleValidationReport + { + public List Issues { get; } = new List(); + + public bool HasErrors + { + get { return Issues.Exists(item => item.Severity == RuleValidationSeverity.Error); } + } + } +} diff --git a/DCIT.Core/Models/RuntimeEnvironmentValidationModels.cs b/DCIT.Core/Models/RuntimeEnvironmentValidationModels.cs new file mode 100644 index 0000000..e4388ec --- /dev/null +++ b/DCIT.Core/Models/RuntimeEnvironmentValidationModels.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace DCIT.Core.Models +{ + public sealed class RuntimeEnvironmentValidationResult + { + public string StartupDirectory { get; set; } + + public bool IsStartupDirectoryWritable { get; set; } + + public string ResolvedAutomationProgId { get; set; } + + public string ResolvedDocToPath { get; set; } + + public IList BlockingIssues { get; } = new List(); + + public IList Warnings { get; } = new List(); + + public bool HasBlockingIssues + { + get { return BlockingIssues.Count > 0; } + } + } +} diff --git a/DCIT.Core/Services/AutoExtractService.cs b/DCIT.Core/Services/AutoExtractService.cs new file mode 100644 index 0000000..c1dc1e7 --- /dev/null +++ b/DCIT.Core/Services/AutoExtractService.cs @@ -0,0 +1,550 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using DCIT.Core.Models; + +namespace DCIT.Core.Services +{ + public class AutoExtractService + { + private static readonly Regex TokenRegex = new Regex( + @"(?[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})|(?[A-Za-z][A-Za-z0-9_\-']*)|(?[\u4E00-\u9FFF]+)", + RegexOptions.Compiled); + + private readonly WordTextExtractionService _wordTextExtractionService; + + public AutoExtractService(WordTextExtractionService wordTextExtractionService) + { + _wordTextExtractionService = wordTextExtractionService; + } + + public Task ExtractAsync( + IList selectedFiles, + AutoExtractSettings settings, + IList existingRules, + IProgress progress, + CancellationToken cancellationToken) + { + return Task.Run( + () => Execute(selectedFiles, settings, existingRules, progress, cancellationToken), + cancellationToken); + } + + private AutoExtractResult Execute( + IList selectedFiles, + AutoExtractSettings settings, + IList existingRules, + IProgress progress, + CancellationToken cancellationToken) + { + var result = new AutoExtractResult(); + var extractedDocuments = new List(); + + if (selectedFiles == null || selectedFiles.Count == 0) + { + return result; + } + + settings = settings ?? new AutoExtractSettings(); + existingRules = existingRules ?? Array.Empty(); + + for (var index = 0; index < selectedFiles.Count; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + var filePath = selectedFiles[index]; + progress?.Report(new ExtractionProgress + { + Current = index + 1, + Total = selectedFiles.Count, + Message = "正在分析 " + filePath, + }); + + try + { + var extracted = _wordTextExtractionService.ExtractVisibleText(filePath); + if (!string.IsNullOrWhiteSpace(extracted.VisibleText)) + { + extractedDocuments.Add(extracted); + result.DocumentsProcessed++; + } + else + { + result.DocumentsSkipped++; + result.Warnings.Add(filePath + " 未提取到可见文本。"); + } + } + catch (Exception ex) + { + result.DocumentsSkipped++; + result.Warnings.Add(filePath + " 提取失败: " + ex.Message); + } + } + + if (extractedDocuments.Count == 0) + { + return result; + } + + var candidateMap = new Dictionary(StringComparer.Ordinal); + var existingSearchTexts = new HashSet( + existingRules + .Where(rule => !string.IsNullOrWhiteSpace(rule.SearchText)) + .Select(rule => rule.SearchText), + StringComparer.Ordinal); + + if (settings.ExtractKeywords) + { + foreach (var keyword in BuildKeywordCandidates(extractedDocuments, settings)) + { + AddCandidate(candidateMap, existingSearchTexts, keyword); + } + } + + if (settings.ExtractHighFrequencyWords) + { + foreach (var word in BuildHighFrequencyWordCandidates(extractedDocuments, settings)) + { + AddCandidate(candidateMap, existingSearchTexts, word); + } + } + + if (settings.ExtractHighFrequencySentences) + { + foreach (var sentence in BuildSentenceCandidates(extractedDocuments, settings)) + { + AddCandidate(candidateMap, existingSearchTexts, sentence); + } + } + + foreach (var candidate in candidateMap.Values.OrderBy(item => item.FirstOccurrenceOrder)) + { + result.Candidates.Add(candidate); + } + + return result; + } + + private IEnumerable BuildKeywordCandidates(IList documents, AutoExtractSettings settings) + { + var tokenObservations = BuildTokenObservations(documents, settings).ToList(); + if (tokenObservations.Count == 0) + { + return Enumerable.Empty(); + } + + var candidates = new List(); + if (settings.UseKeywordFrequency) + { + candidates.AddRange(CreateCandidatesFromRankedEntries( + RankByFrequency(tokenObservations).Take(settings.MaxKeywordCount), + AutoExtractCategory.Keyword, + "关键字(词频)")); + } + + if (settings.UseKeywordTfIdf) + { + candidates.AddRange(CreateCandidatesFromRankedEntries( + RankByTfIdf(tokenObservations).Take(settings.MaxKeywordCount), + AutoExtractCategory.Keyword, + "关键字(TF-IDF)")); + } + + if (settings.UseKeywordTextRank) + { + candidates.AddRange(CreateCandidatesFromRankedEntries( + RankByTextRank(tokenObservations).Take(settings.MaxKeywordCount), + AutoExtractCategory.Keyword, + "关键字(TextRank)")); + } + + return candidates; + } + + private IEnumerable BuildHighFrequencyWordCandidates(IList documents, AutoExtractSettings settings) + { + var tokenObservations = BuildTokenObservations(documents, settings).ToList(); + return CreateCandidatesFromRankedEntries( + RankByFrequency(tokenObservations).Take(settings.MaxWordCount), + AutoExtractCategory.HighFrequencyWord, + "高频词"); + } + + private IEnumerable BuildSentenceCandidates(IList documents, AutoExtractSettings settings) + { + var sentenceMap = new Dictionary(StringComparer.Ordinal); + var order = 0; + var delimiters = new HashSet((settings.SentenceDelimiters ?? string.Empty).ToCharArray()); + + foreach (var document in documents) + { + foreach (var sentence in SplitSentences(document.VisibleText, delimiters)) + { + var trimmed = sentence.Trim(); + if (!IsAllowedSentence(trimmed, settings)) + { + continue; + } + + order++; + RankedEntry entry; + if (!sentenceMap.TryGetValue(trimmed, out entry)) + { + entry = new RankedEntry + { + Text = trimmed, + Count = 0, + Score = 0, + FirstDocumentPath = document.FilePath, + FirstOccurrenceOrder = order, + }; + sentenceMap[trimmed] = entry; + } + + entry.Count++; + entry.Score = entry.Count; + } + } + + return CreateCandidatesFromRankedEntries( + sentenceMap.Values + .OrderByDescending(item => item.Count) + .ThenBy(item => item.FirstOccurrenceOrder) + .Take(settings.MaxSentenceCount), + AutoExtractCategory.HighFrequencySentence, + "高频句"); + } + + private IEnumerable BuildTokenObservations(IList documents, AutoExtractSettings settings) + { + var observations = new List(); + var order = 0; + + for (var documentIndex = 0; documentIndex < documents.Count; documentIndex++) + { + var document = documents[documentIndex]; + foreach (Match match in TokenRegex.Matches(document.VisibleText ?? string.Empty)) + { + if (match.Groups["Email"].Success) + { + if (!settings.ExcludeEmailAddresses) + { + order++; + observations.Add(CreateTokenObservation(match.Value, document.FilePath, documentIndex, order)); + } + + continue; + } + + if (match.Groups["English"].Success) + { + if (settings.CountEnglishWords && match.Value.Length >= settings.MinEnglishWordLength) + { + order++; + observations.Add(CreateTokenObservation(match.Value, document.FilePath, documentIndex, order)); + } + + continue; + } + + if (match.Groups["Chinese"].Success && settings.CountChineseWords) + { + foreach (var token in SegmentChinese(match.Value, settings.MinChineseWordLength)) + { + if (!ContainsChinese(token)) + { + continue; + } + + order++; + observations.Add(CreateTokenObservation(token, document.FilePath, documentIndex, order)); + } + } + } + } + + return observations; + } + + private static IEnumerable SegmentChinese(string text, int minLength) + { + if (string.IsNullOrWhiteSpace(text)) + { + yield break; + } + + var normalized = new string(text.Where(character => character >= 0x4E00 && character <= 0x9FFF).ToArray()); + if (string.IsNullOrWhiteSpace(normalized)) + { + yield break; + } + + var effectiveMinLength = Math.Max(minLength, 1); + var maxLength = Math.Min(4, normalized.Length); + var emitted = false; + + for (var length = maxLength; length >= effectiveMinLength; length--) + { + for (var index = 0; index + length <= normalized.Length; index++) + { + emitted = true; + yield return normalized.Substring(index, length); + } + } + + if (!emitted && normalized.Length >= effectiveMinLength) + { + yield return normalized; + } + } + + private IEnumerable RankByFrequency(IList observations) + { + return observations + .GroupBy(item => item.Token, StringComparer.Ordinal) + .Select(group => new RankedEntry + { + Text = group.Key, + Count = group.Count(), + Score = group.Count(), + FirstDocumentPath = group.OrderBy(item => item.GlobalOrder).First().FilePath, + FirstOccurrenceOrder = group.Min(item => item.GlobalOrder), + }) + .OrderByDescending(item => item.Score) + .ThenBy(item => item.FirstOccurrenceOrder) + .ToList(); + } + + private IEnumerable RankByTfIdf(IList observations) + { + var documentCount = observations.Select(item => item.DocumentIndex).Distinct().Count(); + var byToken = observations.GroupBy(item => item.Token, StringComparer.Ordinal); + var ranked = new List(); + + foreach (var tokenGroup in byToken) + { + var docFrequency = tokenGroup.Select(item => item.DocumentIndex).Distinct().Count(); + var termFrequency = tokenGroup.Count(); + var idf = Math.Log((documentCount + 1.0) / (docFrequency + 1.0)) + 1.0; + ranked.Add(new RankedEntry + { + Text = tokenGroup.Key, + Count = termFrequency, + Score = termFrequency * idf, + FirstDocumentPath = tokenGroup.OrderBy(item => item.GlobalOrder).First().FilePath, + FirstOccurrenceOrder = tokenGroup.Min(item => item.GlobalOrder), + }); + } + + return ranked + .OrderByDescending(item => item.Score) + .ThenBy(item => item.FirstOccurrenceOrder) + .ToList(); + } + + private IEnumerable RankByTextRank(IList observations) + { + var graph = new Dictionary>(StringComparer.Ordinal); + var orderedTokens = observations.OrderBy(item => item.GlobalOrder).ToList(); + const int windowSize = 4; + + for (var index = 0; index < orderedTokens.Count; index++) + { + var current = orderedTokens[index].Token; + if (!graph.ContainsKey(current)) + { + graph[current] = new HashSet(StringComparer.Ordinal); + } + + for (var offset = 1; offset < windowSize && index + offset < orderedTokens.Count; offset++) + { + var neighbor = orderedTokens[index + offset].Token; + if (string.Equals(current, neighbor, StringComparison.Ordinal)) + { + continue; + } + + graph[current].Add(neighbor); + if (!graph.ContainsKey(neighbor)) + { + graph[neighbor] = new HashSet(StringComparer.Ordinal); + } + + graph[neighbor].Add(current); + } + } + + var scores = graph.Keys.ToDictionary(key => key, key => 1.0, StringComparer.Ordinal); + for (var iteration = 0; iteration < 20; iteration++) + { + var next = new Dictionary(StringComparer.Ordinal); + foreach (var node in graph.Keys) + { + var score = 0.15; + foreach (var neighbor in graph[node]) + { + var degree = Math.Max(graph[neighbor].Count, 1); + score += 0.85 * (scores[neighbor] / degree); + } + + next[node] = score; + } + + scores = next; + } + + return observations + .GroupBy(item => item.Token, StringComparer.Ordinal) + .Select(group => new RankedEntry + { + Text = group.Key, + Count = group.Count(), + Score = scores.ContainsKey(group.Key) ? scores[group.Key] : 0, + FirstDocumentPath = group.OrderBy(item => item.GlobalOrder).First().FilePath, + FirstOccurrenceOrder = group.Min(item => item.GlobalOrder), + }) + .OrderByDescending(item => item.Score) + .ThenBy(item => item.FirstOccurrenceOrder) + .ToList(); + } + + private static IEnumerable CreateCandidatesFromRankedEntries( + IEnumerable entries, + AutoExtractCategory category, + string categoryDisplay) + { + return entries.Select(entry => new ExtractionCandidate + { + PrimaryCategory = category, + CategoryDisplay = categoryDisplay, + OriginalText = entry.Text, + StatisticDisplay = string.Format("次数={0}; 分值={1:F3}", entry.Count, entry.Score), + FirstDocumentPath = entry.FirstDocumentPath, + FirstOccurrenceOrder = entry.FirstOccurrenceOrder, + }); + } + + private static IEnumerable SplitSentences(string text, HashSet delimiters) + { + if (string.IsNullOrWhiteSpace(text)) + { + yield break; + } + + var buffer = string.Empty; + foreach (var character in text) + { + if (delimiters.Contains(character)) + { + if (!string.IsNullOrWhiteSpace(buffer)) + { + yield return buffer; + buffer = string.Empty; + } + + continue; + } + + buffer += character; + } + + if (!string.IsNullOrWhiteSpace(buffer)) + { + yield return buffer; + } + } + + private static bool IsAllowedSentence(string sentence, AutoExtractSettings settings) + { + if (string.IsNullOrWhiteSpace(sentence)) + { + return false; + } + + var hasChinese = ContainsChinese(sentence); + var hasEnglish = sentence.Any(character => (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z')); + + if (hasChinese && settings.CountChineseSentences) + { + return true; + } + + if (hasEnglish && settings.CountEnglishSentences) + { + return true; + } + + return !hasChinese && !hasEnglish; + } + + private static bool ContainsChinese(string value) + { + return value.Any(character => character >= 0x4E00 && character <= 0x9FFF); + } + + private static TokenObservation CreateTokenObservation(string token, string filePath, int documentIndex, int order) + { + return new TokenObservation + { + Token = token, + FilePath = filePath, + DocumentIndex = documentIndex, + GlobalOrder = order, + }; + } + + private static void AddCandidate( + IDictionary candidateMap, + ISet existingSearchTexts, + ExtractionCandidate candidate) + { + if (string.IsNullOrWhiteSpace(candidate.OriginalText)) + { + return; + } + + if (existingSearchTexts.Contains(candidate.OriginalText)) + { + return; + } + + ExtractionCandidate existing; + if (!candidateMap.TryGetValue(candidate.OriginalText, out existing)) + { + candidateMap[candidate.OriginalText] = candidate; + return; + } + + if (existing.CategoryDisplay.IndexOf(candidate.CategoryDisplay, StringComparison.OrdinalIgnoreCase) < 0) + { + existing.CategoryDisplay += ", " + candidate.CategoryDisplay; + } + } + + private class TokenObservation + { + public string Token { get; set; } + + public string FilePath { get; set; } + + public int DocumentIndex { get; set; } + + public int GlobalOrder { get; set; } + } + + private class RankedEntry + { + public string Text { get; set; } + + public int Count { get; set; } + + public double Score { get; set; } + + public string FirstDocumentPath { get; set; } + + public int FirstOccurrenceOrder { get; set; } + } + } +} diff --git a/DCIT.Core/Services/BatchExecutionPlanner.cs b/DCIT.Core/Services/BatchExecutionPlanner.cs new file mode 100644 index 0000000..5850ab1 --- /dev/null +++ b/DCIT.Core/Services/BatchExecutionPlanner.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using DCIT.Core.Models; + +namespace DCIT.Core.Services +{ + public sealed class BatchExecutionPlanner + { + public BatchExecutionPlan CreatePlan(IList selectedItems, int processorCount) + { + var executionItems = new List(); + var skippedItems = new List(); + var sourcePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var outputPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var diffPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (selectedItems != null) + { + foreach (var item in selectedItems) + { + if (item == null) + { + continue; + } + + var sourcePath = NormalizePath(item.SourcePath); + var outputPath = NormalizePath(item.OutputPath); + var diffPath = NormalizePath(item.DiffPath); + + var collision = GetCollision(sourcePath, sourcePaths, BatchExecutionSkipReason.DuplicateSourcePath); + if (collision == null) + { + collision = GetCollision(outputPath, outputPaths, BatchExecutionSkipReason.DuplicateOutputPath); + } + + if (collision == null && !string.IsNullOrWhiteSpace(diffPath)) + { + collision = GetCollision(diffPath, diffPaths, BatchExecutionSkipReason.DuplicateDiffPath); + } + + if (collision != null) + { + collision.Item = item; + skippedItems.Add(collision); + continue; + } + + AddIfPresent(sourcePaths, sourcePath); + AddIfPresent(outputPaths, outputPath); + AddIfPresent(diffPaths, diffPath); + executionItems.Add(item); + } + } + + var requiresSerialExecution = executionItems.Any(item => Path.GetExtension(item.SourcePath).Equals(".doc", StringComparison.OrdinalIgnoreCase)); + var degreeOfParallelism = DetermineDegreeOfParallelism(executionItems.Count, processorCount, requiresSerialExecution); + + return new BatchExecutionPlan + { + ExecutionItems = executionItems, + SkippedItems = skippedItems, + DegreeOfParallelism = degreeOfParallelism, + RequiresSerialExecution = requiresSerialExecution, + }; + } + + private static BatchExecutionSkipItem GetCollision(string path, HashSet existingPaths, BatchExecutionSkipReason reason) + { + if (string.IsNullOrWhiteSpace(path) || !existingPaths.Contains(path)) + { + return null; + } + + return new BatchExecutionSkipItem + { + Reason = reason, + ConflictingPath = path, + ExistingPath = path, + }; + } + + private static void AddIfPresent(HashSet set, string path) + { + if (!string.IsNullOrWhiteSpace(path)) + { + set.Add(path); + } + } + + private static int DetermineDegreeOfParallelism(int itemCount, int processorCount, bool requiresSerialExecution) + { + if (itemCount <= 0 || requiresSerialExecution) + { + return 1; + } + + var normalizedProcessorCount = processorCount > 0 ? processorCount : Environment.ProcessorCount; + normalizedProcessorCount = Math.Max(1, normalizedProcessorCount); + return Math.Max(1, Math.Min(itemCount, normalizedProcessorCount)); + } + + private static string NormalizePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return string.Empty; + } + + try + { + return Path.GetFullPath(path) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + catch + { + return path.Trim(); + } + } + } +} diff --git a/DCIT.Core/Services/BmpDiffCodec.cs b/DCIT.Core/Services/BmpDiffCodec.cs new file mode 100644 index 0000000..31e19c5 --- /dev/null +++ b/DCIT.Core/Services/BmpDiffCodec.cs @@ -0,0 +1,240 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using DCIT.Core.Utilities; + +namespace DCIT.Core.Services +{ + public class BmpDiffCodec + { + private const string Magic = "DCITBMP1"; + private const ushort HeaderVersion = 1; + private const ushort Flags = 0; + private const int FixedWidth = 1024; + private const int HeaderSize = 8 + 2 + 2 + 8 + 32; + + public void EncodeJson(string canonicalJson, string outputPath) + { + var payloadBytes = Encoding.UTF8.GetBytes(canonicalJson ?? string.Empty); + using (var stream = new MemoryStream(payloadBytes)) + { + EncodeFromStream(stream, outputPath); + } + } + + public void EncodeFromStream(Stream jsonStream, string outputPath) + { + if (jsonStream == null) + { + throw new ArgumentNullException("jsonStream"); + } + + byte[] hash; + long length; + + if (jsonStream.CanSeek) + { + length = jsonStream.Length; + using (var sha = SHA256.Create()) + { + hash = sha.ComputeHash(jsonStream); + } + + jsonStream.Position = 0; + } + else + { + using (var buffered = new MemoryStream()) + { + jsonStream.CopyTo(buffered); + length = buffered.Length; + using (var sha = SHA256.Create()) + { + hash = sha.ComputeHash(buffered); + } + + buffered.Position = 0; + WriteBitmapWithPayload(outputPath, length, hash, buffered); + return; + } + } + + WriteBitmapWithPayload(outputPath, length, hash, jsonStream); + } + + public string DecodeJson(string bmpPath) + { + using (var bitmap = new Bitmap(bmpPath)) + { + var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + var bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, bitmap.PixelFormat); + try + { + var rowLength = bitmap.Width; + var pixelBytes = new byte[rowLength * bitmap.Height]; + for (var row = 0; row < bitmap.Height; row++) + { + var source = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride); + Marshal.Copy(source, pixelBytes, row * rowLength, rowLength); + } + + var payloadBytes = ParseBlob(pixelBytes); + return Encoding.UTF8.GetString(payloadBytes); + } + finally + { + bitmap.UnlockBits(bitmapData); + } + } + } + + private static void WriteBitmapWithPayload(string outputPath, long payloadLength, byte[] payloadHash, Stream payloadStream) + { + var totalBytes = HeaderSize + payloadLength; + var height = Math.Max(1, (int)Math.Ceiling(totalBytes / (double)FixedWidth)); + var pixelCount = FixedWidth * height; + var pixelBytes = new byte[pixelCount]; + + var offset = 0; + var magicBytes = Encoding.ASCII.GetBytes(Magic); + Buffer.BlockCopy(magicBytes, 0, pixelBytes, offset, magicBytes.Length); + offset += magicBytes.Length; + + pixelBytes[offset++] = (byte)(HeaderVersion & 0xFF); + pixelBytes[offset++] = (byte)((HeaderVersion >> 8) & 0xFF); + + pixelBytes[offset++] = (byte)(Flags & 0xFF); + pixelBytes[offset++] = (byte)((Flags >> 8) & 0xFF); + + pixelBytes[offset++] = (byte)(payloadLength & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 8) & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 16) & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 24) & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 32) & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 40) & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 48) & 0xFF); + pixelBytes[offset++] = (byte)((payloadLength >> 56) & 0xFF); + + Buffer.BlockCopy(payloadHash, 0, pixelBytes, offset, 32); + offset += 32; + + var buffer = new byte[65536]; + int read; + while ((read = payloadStream.Read(buffer, 0, buffer.Length)) > 0) + { + if (offset + read > pixelBytes.Length) + { + throw new InvalidDataException("BMP 载荷长度超出预期像素缓冲。"); + } + + Buffer.BlockCopy(buffer, 0, pixelBytes, offset, read); + offset += read; + } + + if (offset != totalBytes) + { + throw new InvalidDataException("BMP 载荷长度与声明的长度不一致。"); + } + + using (var bitmap = new Bitmap(FixedWidth, height, PixelFormat.Format8bppIndexed)) + { + var palette = bitmap.Palette; + for (var index = 0; index < 256; index++) + { + palette.Entries[index] = Color.FromArgb(index, index, index); + } + + bitmap.Palette = palette; + var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); + try + { + for (var row = 0; row < height; row++) + { + var sourceOffset = row * FixedWidth; + var destination = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride); + Marshal.Copy(pixelBytes, sourceOffset, destination, FixedWidth); + } + } + finally + { + bitmap.UnlockBits(bitmapData); + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? string.Empty); + bitmap.Save(outputPath, ImageFormat.Bmp); + } + } + + private static byte[] BuildBlob(byte[] payloadBytes) + { + payloadBytes = payloadBytes ?? new byte[0]; + var payloadHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes)); + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream, Encoding.ASCII)) + { + writer.Write(Encoding.ASCII.GetBytes(Magic)); + writer.Write(HeaderVersion); + writer.Write(Flags); + writer.Write((ulong)payloadBytes.Length); + writer.Write(payloadHash); + writer.Write(payloadBytes); + writer.Flush(); + return stream.ToArray(); + } + } + + private static byte[] ParseBlob(byte[] blobBytes) + { + using (var stream = new MemoryStream(blobBytes)) + using (var reader = new BinaryReader(stream, Encoding.ASCII)) + { + var magic = Encoding.ASCII.GetString(reader.ReadBytes(8)); + if (!string.Equals(magic, Magic, StringComparison.Ordinal)) + { + throw new InvalidDataException("BMP 差异文件魔数无效。"); + } + + var version = reader.ReadUInt16(); + if (version != HeaderVersion) + { + throw new InvalidDataException("BMP 差异文件版本不受支持。"); + } + + _ = reader.ReadUInt16(); + var payloadLength = reader.ReadUInt64(); + var payloadHash = reader.ReadBytes(32); + var payloadBytes = reader.ReadBytes((int)payloadLength); + var actualHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes)); + if (!BytesEqual(payloadHash, actualHash)) + { + throw new InvalidDataException("BMP 差异文件载荷校验失败。"); + } + + return payloadBytes; + } + } + + private static bool BytesEqual(byte[] left, byte[] right) + { + if (left == null || right == null || left.Length != right.Length) + { + return false; + } + + for (var index = 0; index < left.Length; index++) + { + if (left[index] != right[index]) + { + return false; + } + } + + return true; + } + } +} diff --git a/DCIT.Core/Services/ConfigurationService.cs b/DCIT.Core/Services/ConfigurationService.cs new file mode 100644 index 0000000..b1a2e61 --- /dev/null +++ b/DCIT.Core/Services/ConfigurationService.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Text; +using DCIT.Core.Models; +using Newtonsoft.Json; + +namespace DCIT.Core.Services +{ + public class ConfigurationService + { + private readonly string _configPath; + + public ConfigurationService(string startupDirectory) + { + _configPath = Path.Combine(startupDirectory, "config.json"); + } + + public string ConfigPath + { + get { return _configPath; } + } + + public ConfigurationLoadResult Load() + { + if (!File.Exists(_configPath)) + { + return new ConfigurationLoadResult + { + Config = CreateDefault(), + IsMissing = true, + }; + } + + try + { + var content = File.ReadAllText(_configPath, Encoding.UTF8); + var config = JsonConvert.DeserializeObject(content) ?? CreateDefault(); + return new ConfigurationLoadResult + { + Config = config, + }; + } + catch (Exception ex) + { + return new ConfigurationLoadResult + { + Config = CreateDefault(), + IsCorrupted = true, + ErrorMessage = ex.Message, + }; + } + } + + public void Save(AppConfig config) + { + config.SavedAt = DateTime.Now; + var content = JsonConvert.SerializeObject(config, Formatting.Indented); + File.WriteAllText(_configPath, content, new UTF8Encoding(false)); + } + + public AppConfig CreateDefault() + { + return new AppConfig(); + } + } +} diff --git a/DCIT.Core/Services/DiffSerializationService.cs b/DCIT.Core/Services/DiffSerializationService.cs new file mode 100644 index 0000000..b1a82f4 --- /dev/null +++ b/DCIT.Core/Services/DiffSerializationService.cs @@ -0,0 +1,592 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using DCIT.Core.Models; +using DCIT.Core.Utilities; +using Newtonsoft.Json; + +namespace DCIT.Core.Services +{ + public class DiffSerializationService + { + private static readonly byte[] PayloadCiphertextPropertyMarker = Encoding.UTF8.GetBytes("\"PayloadCiphertext\":"); + private static readonly byte[] PayloadCiphertextValueMarker = Encoding.UTF8.GetBytes("\"PayloadCiphertext\":\""); + private static readonly byte[] MetadataSyntheticSuffix = Encoding.UTF8.GetBytes("null,\"Payload\":null}"); + private const int MetadataScanCapBytes = 1 << 20; + + private readonly BmpDiffCodec _bmpDiffCodec; + private readonly string _appVersion; + + public DiffSerializationService(BmpDiffCodec bmpDiffCodec) + { + _bmpDiffCodec = bmpDiffCodec; + _appVersion = Assembly.GetExecutingAssembly().GetName().Version != null + ? Assembly.GetExecutingAssembly().GetName().Version.ToString() + : "1.0.0"; + } + + public string AppVersion + { + get { return _appVersion; } + } + + public string GetBmpPath(string diffPath) + { + return Path.ChangeExtension(diffPath, ".bmp"); + } + + public string Write(DiffDocument plainDiff, string diffPath, bool encryptJson) + { + if (plainDiff == null) + { + throw new ArgumentNullException("plainDiff"); + } + + if (plainDiff.Payload == null) + { + throw new InvalidOperationException("差异载荷不能为空。"); + } + + var originalPayload = plainDiff.Payload; + var originalEncrypted = plainDiff.Encrypted; + var originalEncryption = plainDiff.Encryption; + var originalPayloadCiphertext = plainDiff.PayloadCiphertext; + + string payloadTmpPath = null; + string cipherTmpPath = null; + + try + { + payloadTmpPath = Path.Combine(Path.GetTempPath(), "dcit-payload-" + Guid.NewGuid().ToString("N") + ".bin"); + SerializeInstanceToUtf8File(plainDiff.Payload, payloadTmpPath); + plainDiff.Integrity.PayloadHash = HashUtility.ComputeFileSha256Base64(payloadTmpPath); + plainDiff.Integrity.TamperProtectionValue = HashUtility.ComputeFileHmacBase64(payloadTmpPath); + + plainDiff.Encrypted = false; + plainDiff.Encryption = null; + plainDiff.PayloadCiphertext = null; + + Directory.CreateDirectory(Path.GetDirectoryName(diffPath) ?? string.Empty); + var bmpPath = GetBmpPath(diffPath); + + // Serialize the full plainDiff (with Payload attached) to BMP. Use a temp file so we + // never hold the whole BMP-bound JSON in memory. + var bmpTmpPath = Path.Combine(Path.GetTempPath(), "dcit-bmp-" + Guid.NewGuid().ToString("N") + ".json"); + try + { + SerializeInstanceToUtf8File(plainDiff, bmpTmpPath); + using (var bmpFile = File.OpenRead(bmpTmpPath)) + { + _bmpDiffCodec.EncodeFromStream(bmpFile, bmpPath); + } + } + finally + { + TryDeleteFile(bmpTmpPath); + } + + if (encryptJson) + { + cipherTmpPath = Path.Combine(Path.GetTempPath(), "dcit-cipher-" + Guid.NewGuid().ToString("N") + ".bin"); + EncryptedPayloadHeader header; + using (var payloadStream = File.OpenRead(payloadTmpPath)) + using (var cipherStream = File.Create(cipherTmpPath)) + { + header = HashUtility.EncryptStream(payloadStream, cipherStream); + } + + plainDiff.Encrypted = true; + plainDiff.Encryption = new DiffEncryptionInfo + { + Algorithm = "AES-256-CBC+HMAC-SHA256", + KeyId = "default", + Nonce = header.NonceBase64, + Tag = header.TagBase64, + }; + plainDiff.PayloadCiphertext = null; + plainDiff.Payload = null; + + WriteEncryptedDiffToFile(plainDiff, diffPath, cipherTmpPath); + } + else + { + SerializeToFile(plainDiff, diffPath); + } + + return bmpPath; + } + finally + { + plainDiff.Payload = originalPayload; + plainDiff.Encrypted = originalEncrypted; + plainDiff.Encryption = originalEncryption; + plainDiff.PayloadCiphertext = originalPayloadCiphertext; + + TryDeleteFile(payloadTmpPath); + TryDeleteFile(cipherTmpPath); + } + } + + private static void SerializeInstanceToUtf8File(object instance, string path) + { + using (var stream = File.Create(path)) + using (var streamWriter = new StreamWriter(stream, new UTF8Encoding(false), bufferSize: 65536)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) + { + var serializer = JsonSerializer.CreateDefault(); + serializer.Formatting = Formatting.None; + serializer.Serialize(jsonWriter, instance); + jsonWriter.Flush(); + } + } + + private static void WriteEncryptedDiffToFile(DiffDocument document, string diffPath, string cipherTempPath) + { + var metadataJson = JsonConvert.SerializeObject(document, Formatting.None); + const string placeholder = "\"PayloadCiphertext\":null"; + var placeholderIndex = metadataJson.IndexOf(placeholder, StringComparison.Ordinal); + if (placeholderIndex < 0) + { + throw new InvalidOperationException("加密差异元数据缺少 PayloadCiphertext 占位符。"); + } + + using (var fileStream = File.Create(diffPath)) + using (var writer = new StreamWriter(fileStream, new UTF8Encoding(false), bufferSize: 65536)) + { + writer.Write(metadataJson.Substring(0, placeholderIndex)); + writer.Write("\"PayloadCiphertext\":\""); + using (var cipherStream = File.OpenRead(cipherTempPath)) + { + var buffer = new byte[30720]; + int read; + while ((read = cipherStream.Read(buffer, 0, buffer.Length)) > 0) + { + writer.Write(Convert.ToBase64String(buffer, 0, read)); + } + } + + writer.Write('"'); + writer.Write(metadataJson.Substring(placeholderIndex + placeholder.Length)); + } + } + + private static void TryDeleteFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + // Best-effort cleanup; temp files live in the system temp dir. + } + } + + public LoadedDiffDocument Load(string diffOrBmpPath) + { + if (diffOrBmpPath == null) + { + throw new ArgumentNullException("diffOrBmpPath"); + } + + if (Path.GetExtension(diffOrBmpPath).Equals(".bmp", StringComparison.OrdinalIgnoreCase)) + { + return LoadFromBmp(diffOrBmpPath); + } + + return LoadFromDiff(diffOrBmpPath); + } + + public DiffDocument ReadMetadata(string diffOrBmpPath) + { + if (diffOrBmpPath == null) + { + throw new ArgumentNullException("diffOrBmpPath"); + } + + if (Path.GetExtension(diffOrBmpPath).Equals(".bmp", StringComparison.OrdinalIgnoreCase)) + { + var json = _bmpDiffCodec.DecodeJson(diffOrBmpPath); + return JsonConvert.DeserializeObject(json) ?? new DiffDocument(); + } + + return ReadDiffMetadata(diffOrBmpPath); + } + + public DiffDocument CreateBaseDocument() + { + return new DiffDocument + { + App = new DiffAppInfo + { + Name = "DCIT", + Version = _appVersion, + }, + Algorithm = new DiffAlgorithmInfo + { + DiffVersion = "1.0", + BmpCodecVersion = "1.0", + }, + }; + } + + // BMP diffs use a legacy in-memory load path. Large BMP-encoded diffs remain a known + // OOM risk; the default and reported path is .diff, which is fully streaming below. + private LoadedDiffDocument LoadFromBmp(string bmpPath) + { + var json = _bmpDiffCodec.DecodeJson(bmpPath); + var document = JsonConvert.DeserializeObject(json); + if (document == null) + { + throw new InvalidDataException("差异文件解析失败。"); + } + + if (document.Encrypted) + { + if (document.Encryption == null || string.IsNullOrWhiteSpace(document.PayloadCiphertext)) + { + throw new InvalidDataException("加密差异文件缺少必要字段。"); + } + + var payloadBytes = HashUtility.Decrypt( + document.PayloadCiphertext, + document.Encryption.Nonce, + document.Encryption.Tag); + var payloadJson = Encoding.UTF8.GetString(payloadBytes); + document.Payload = JsonConvert.DeserializeObject(payloadJson); + ValidateIntegrity(document, payloadBytes); + } + else + { + if (document.Payload == null) + { + throw new InvalidDataException("明文差异文件缺少 payload。"); + } + + ValidateIntegrity(document, Encoding.UTF8.GetBytes(SerializeCanonical(document.Payload))); + } + + return new LoadedDiffDocument + { + Document = document, + }; + } + + private LoadedDiffDocument LoadFromDiff(string diffPath) + { + var document = ReadDiffMetadata(diffPath); + string payloadTmpPath = null; + string cipherTmpPath = null; + + try + { + if (document.Encrypted) + { + if (document.Encryption == null + || string.IsNullOrWhiteSpace(document.Encryption.Nonce) + || string.IsNullOrWhiteSpace(document.Encryption.Tag)) + { + throw new InvalidDataException("加密差异文件缺少加密元数据。"); + } + + cipherTmpPath = Path.Combine(Path.GetTempPath(), "dcit-rcipher-" + Guid.NewGuid().ToString("N") + ".bin"); + payloadTmpPath = Path.Combine(Path.GetTempPath(), "dcit-rpayload-" + Guid.NewGuid().ToString("N") + ".bin"); + + ExtractBase64RegionToFile(diffPath, PayloadCiphertextValueMarker, cipherTmpPath); + + var iv = Convert.FromBase64String(document.Encryption.Nonce); + var tag = Convert.FromBase64String(document.Encryption.Tag); + using (var cipherStream = File.OpenRead(cipherTmpPath)) + using (var payloadStream = File.Create(payloadTmpPath)) + { + HashUtility.DecryptStream(cipherStream, payloadStream, iv, tag); + } + + ValidateIntegrityFromPayloadFile(document, payloadTmpPath); + document.Payload = DeserializePayloadFromFile(payloadTmpPath); + } + else + { + // Stream-deserialize the whole document. PayloadCiphertext is null (small) and + // Payload is a large nested object that Newtonsoft builds incrementally without + // buffering the whole JSON text or any single huge string token. + var full = StreamDeserializeDiffDocument(diffPath); + if (full == null) + { + throw new InvalidDataException("差异文件解析失败。"); + } + + if (full.Payload == null) + { + throw new InvalidDataException("明文差异文件缺少 payload。"); + } + + document = full; + payloadTmpPath = Path.Combine(Path.GetTempPath(), "dcit-rpayload-" + Guid.NewGuid().ToString("N") + ".bin"); + SerializeInstanceToUtf8File(document.Payload, payloadTmpPath); + ValidateIntegrityFromPayloadFile(document, payloadTmpPath); + } + + return new LoadedDiffDocument + { + Document = document, + }; + } + finally + { + TryDeleteFile(payloadTmpPath); + TryDeleteFile(cipherTmpPath); + } + } + + // Reads only the metadata portion of a .diff file. The huge fields (PayloadCiphertext value + // and Payload value) sit after the metadata in declaration order; we byte-scan up to the + // "PayloadCiphertext" property marker (a few KB), synthesize null values for the two huge + // fields, and parse the resulting small metadata JSON. The huge content is never read. + private DiffDocument ReadDiffMetadata(string diffPath) + { + var prefix = ReadPrefixThroughMarker(diffPath, PayloadCiphertextPropertyMarker, MetadataScanCapBytes); + string metadataJson; + if (prefix == null) + { + metadataJson = File.ReadAllText(diffPath, Encoding.UTF8); + } + else + { + var combined = new byte[prefix.Length + MetadataSyntheticSuffix.Length]; + Buffer.BlockCopy(prefix, 0, combined, 0, prefix.Length); + Buffer.BlockCopy(MetadataSyntheticSuffix, 0, combined, prefix.Length, MetadataSyntheticSuffix.Length); + metadataJson = Encoding.UTF8.GetString(combined); + } + + return JsonConvert.DeserializeObject(metadataJson) ?? new DiffDocument(); + } + + private static DiffDocument StreamDeserializeDiffDocument(string diffPath) + { + using (var stream = File.OpenRead(diffPath)) + using (var streamReader = new StreamReader(stream, new UTF8Encoding(false))) + using (var jsonReader = new JsonTextReader(streamReader)) + { + return JsonSerializer.CreateDefault().Deserialize(jsonReader); + } + } + + private static DiffPayload DeserializePayloadFromFile(string payloadPath) + { + using (var stream = File.OpenRead(payloadPath)) + using (var streamReader = new StreamReader(stream, new UTF8Encoding(false))) + using (var jsonReader = new JsonTextReader(streamReader)) + { + return JsonSerializer.CreateDefault().Deserialize(jsonReader); + } + } + + private static void ValidateIntegrity(DiffDocument document, byte[] payloadBytes) + { + var actualHash = HashUtility.ComputeSha256Base64(payloadBytes); + var actualTamper = HashUtility.ComputeHmacBase64(payloadBytes); + if (!string.Equals(actualHash, document.Integrity == null ? null : document.Integrity.PayloadHash, StringComparison.Ordinal)) + { + throw new InvalidDataException("差异文件载荷哈希校验失败。"); + } + + if (!string.Equals(actualTamper, document.Integrity == null ? null : document.Integrity.TamperProtectionValue, StringComparison.Ordinal)) + { + throw new InvalidDataException("差异文件防篡改校验失败。"); + } + } + + private static void ValidateIntegrityFromPayloadFile(DiffDocument document, string payloadPath) + { + var actualHash = HashUtility.ComputeFileSha256Base64(payloadPath); + var actualTamper = HashUtility.ComputeFileHmacBase64(payloadPath); + if (!string.Equals(actualHash, document.Integrity == null ? null : document.Integrity.PayloadHash, StringComparison.Ordinal)) + { + throw new InvalidDataException("差异文件载荷哈希校验失败。"); + } + + if (!string.Equals(actualTamper, document.Integrity == null ? null : document.Integrity.TamperProtectionValue, StringComparison.Ordinal)) + { + throw new InvalidDataException("差异文件防篡改校验失败。"); + } + } + + private static string SerializeCanonical(object instance) + { + return JsonConvert.SerializeObject(instance, Formatting.None); + } + + private static void SerializeToFile(object instance, string path) + { + using (var stream = File.Create(path)) + using (var streamWriter = new StreamWriter(stream, new UTF8Encoding(false), bufferSize: 65536)) + using (var jsonWriter = new JsonTextWriter(streamWriter)) + { + var serializer = JsonSerializer.CreateDefault(); + serializer.Formatting = Formatting.None; + serializer.Serialize(jsonWriter, instance); + jsonWriter.Flush(); + } + } + + // Reads bytes from the start of the file up to and including the first occurrence of marker. + // Returns null if the marker is not found within cap bytes. Caller chooses cap so the marker + // (which sits after the small metadata block) is found without reading huge payload content. + private static byte[] ReadPrefixThroughMarker(string path, byte[] marker, int cap) + { + using (var stream = File.OpenRead(path)) + { + var ms = new MemoryStream(); + var buffer = new byte[65536]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + if (ms.Length >= cap) + { + break; + } + } + + var arr = ms.ToArray(); + var idx = IndexOfSubarray(arr, marker); + if (idx < 0) + { + return null; + } + + var result = new byte[idx + marker.Length]; + Buffer.BlockCopy(arr, 0, result, 0, result.Length); + return result; + } + } + + // Locates valueMarker in the file, then stream-decodes the base64 region between the marker + // and the next closing double-quote into outputPath. Decodes in 4096-char chunks (a multiple + // of 4) so there is no mid-chunk base64 alignment error. + private static void ExtractBase64RegionToFile(string diffPath, byte[] valueMarker, string outputPath) + { + long markerEndPosition; + using (var probe = File.OpenRead(diffPath)) + { + markerEndPosition = FindMarkerEnd(probe, valueMarker); + } + + if (markerEndPosition < 0) + { + throw new InvalidDataException("差异文件缺少密文字段标记。"); + } + + using (var stream = File.OpenRead(diffPath)) + { + stream.Position = markerEndPosition; + using (var outStream = File.Create(outputPath)) + { + var charBuf = new char[4096]; + var charLen = 0; + var byteBuf = new byte[65536]; + int read; + var done = false; + while (!done && (read = stream.Read(byteBuf, 0, byteBuf.Length)) > 0) + { + for (var i = 0; i < read; i++) + { + var b = byteBuf[i]; + if (b == '"') + { + done = true; + break; + } + + charBuf[charLen++] = (char)b; + if (charLen == charBuf.Length) + { + var decoded = Convert.FromBase64CharArray(charBuf, 0, charLen); + outStream.Write(decoded, 0, decoded.Length); + charLen = 0; + } + } + } + + if (charLen > 0) + { + var decoded = Convert.FromBase64CharArray(charBuf, 0, charLen); + outStream.Write(decoded, 0, decoded.Length); + } + } + } + } + + private static long FindMarkerEnd(Stream stream, byte[] marker) + { + var ms = new MemoryStream(); + var buffer = new byte[65536]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + var arr = ms.ToArray(); + var idx = IndexOfSubarray(arr, marker); + if (idx >= 0) + { + return idx + marker.Length; + } + + if (ms.Length > MetadataScanCapBytes) + { + return -1; + } + } + + return -1; + } + + private static int IndexOfSubarray(byte[] haystack, byte[] needle) + { + if (needle == null || needle.Length == 0) + { + return 0; + } + + if (haystack == null || haystack.Length < needle.Length) + { + return -1; + } + + for (var i = 0; i <= haystack.Length - needle.Length; i++) + { + var match = true; + for (var j = 0; j < needle.Length; j++) + { + if (haystack[i + j] != needle[j]) + { + match = false; + break; + } + } + + if (match) + { + return i; + } + } + + return -1; + } + } + + public class LoadedDiffDocument + { + public DiffDocument Document { get; set; } + } +} diff --git a/DCIT.Core/Services/DocumentProcessingService.cs b/DCIT.Core/Services/DocumentProcessingService.cs new file mode 100644 index 0000000..77f49d2 --- /dev/null +++ b/DCIT.Core/Services/DocumentProcessingService.cs @@ -0,0 +1,4434 @@ +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 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() : 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(); + 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 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(); + 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(); + } + + 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 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 ScanBrokenFieldResults(string workingDocxPath) + { + var findings = new List(); + 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 baselineFindings, + string operationName, + IProgress progress) + { + if (string.IsNullOrWhiteSpace(workingDocxPath) || !File.Exists(workingDocxPath)) + { + return; + } + + var currentFindings = ScanBrokenFieldResults(workingDocxPath); + if (currentFindings.Count == 0) + { + return; + } + + var baselineIdentities = new HashSet( + (baselineFindings ?? Enumerable.Empty()).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()) + { + 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()) + { + if (sb.Length > 0) + { + sb.Append(" | "); + } + + var firstPara = cell.Descendants().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().ToList(); + hitColumnIndex = cells.TakeWhile(c => !ReferenceEquals(c, hitCell)).Count(); + var rows = table.Elements().ToList(); + hitRowIndex = rows.TakeWhile(r => !ReferenceEquals(r, row)).Count(); + } + + var sb = new StringBuilder(); + var rowIdx = 0; + foreach (var currentRow in table.Elements()) + { + var cells = currentRow.Elements().ToList(); + if (hitColumnIndex < cells.Count) + { + if (sb.Length > 0) + { + sb.Append(" | "); + } + + var firstPara = cells[hitColumnIndex].Descendants().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 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 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(); + var visited = new HashSet(); + 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 EnumerateImageParts(WordprocessingDocument document) + { + foreach (var partContext in EnumerateTextParts(document)) + { + var drawings = partContext.Root.Descendants().ToList(); + for (var index = 0; index < drawings.Count; index++) + { + var drawing = drawings[index]; + var blip = drawing.Descendants() + .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().Any()) + || item is W.EmbeddedObject) + .ToList(); + for (var index = 0; index < vmlContainers.Count; index++) + { + var containerElement = vmlContainers[index]; + var imageData = containerElement.Descendants() + .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(); + var anchor = drawing.GetFirstChild(); + 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().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().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().Any()) + { + return "square"; + } + + if (anchor.Elements().Any()) + { + return "tight"; + } + + if (anchor.Elements().Any()) + { + return "through"; + } + + if (anchor.Elements().Any()) + { + return "topBottom"; + } + + if (anchor.Elements().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 ParseVmlStyle(string style) + { + var map = new Dictionary(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 styleMap, string key) + { + if (styleMap == null || !styleMap.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue)) + { + return 0L; + } + + var match = Regex.Match(rawValue.Trim(), @"^(?-?\d+(?:\.\d+)?)(?[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 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 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 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 rules, + DiffPayload payload, + ISet touchedRoots, + IProgress 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 progress, + string filePath, + CancellationToken cancellationToken, + ISet 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(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(StringComparer.OrdinalIgnoreCase); + + var processedRelations = new HashSet(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 images, + DiffOperation operation, + IDictionary imageBytesCache, + ISet 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 images, + DiffOperation operation, + IDictionary imageBytesCache, + ISet 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 imageBytesCache, + ISet 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 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 ""; + } + + 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) : "", + 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(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()), + }; + } + + 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(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 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 CreateNoiseCells(int width, int height, int target, Random random) + { + var total = width * height; + var assigned = 0; + var cells = new List(); + + 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().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().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 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()) + { + yield return BuildParagraphTarget(context, paragraph); + } + + yield break; + } + + foreach (var paragraph in context.Root.Descendants()) + { + 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(); + 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() + .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(); + var runList = new List(); + var runOffsets = new Dictionary(); + var logicalWordBoundaries = new HashSet(); + 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 leaves, + StringBuilder textBuilder, + IList runList, + IDictionary runOffsets, + ISet 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().Any()) + { + return false; + } + + var run = text.Ancestors().FirstOrDefault(); + if (run != null && run.RunProperties != null && run.RunProperties.Vanish != null) + { + return false; + } + + return true; + } + + private static List FindMatches(ParagraphState state, RuleDefinition rule) + { + var matches = new List(); + 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 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().Any()) + { + return "tableCell"; + } + + if (target.ParagraphElement.Descendants().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().Any()) + { + return true; + } + + var run = leaf.Run as W.Run; + if (run == null) + { + return false; + } + + return run.Descendants().Any() + || run.ElementsBefore().OfType().Any(item => item.Descendants().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.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 ""; + } + + 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 ?? "", + position.StartTextOffset, + position.EndTextOffset, + position.StartRunIndex, + position.StartCharOffset, + position.EndRunIndex, + position.EndCharOffset, + position.ObjectIndex.HasValue ? position.ObjectIndex.Value.ToString(CultureInfo.InvariantCulture) : ""); + } + + 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> 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(); + 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 leaves) + { + if (leaves == null) + { + return ""; + } + + 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 Leaves { get; set; } + + public ISet 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 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); + } + } +} \ No newline at end of file diff --git a/DCIT.Core/Services/FileScanService.cs b/DCIT.Core/Services/FileScanService.cs new file mode 100644 index 0000000..9942305 --- /dev/null +++ b/DCIT.Core/Services/FileScanService.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using DCIT.Core.Models; +using DCIT.Core.Utilities; + +namespace DCIT.Core.Services +{ + public class FileScanService + { + private static readonly TimeSpan RuleMatchTimeout = TimeSpan.FromSeconds(1); + private readonly DiffSerializationService _diffSerializationService; + + public FileScanService() + : this(new DiffSerializationService(new BmpDiffCodec())) + { + } + + public FileScanService(DiffSerializationService diffSerializationService) + { + if (diffSerializationService == null) + { + throw new ArgumentNullException("diffSerializationService"); + } + + _diffSerializationService = diffSerializationService; + } + + public IList ScanReplaceCandidates(string sourceDirectory, string outputDirectory) + { + return ScanReplaceCandidates(sourceDirectory, outputDirectory, null); + } + + public IList ScanReplaceCandidates(string sourceDirectory, string outputDirectory, RuleFile ruleFile) + { + var items = new List(); + var hasOutputDirectory = !string.IsNullOrWhiteSpace(outputDirectory); + var rules = GetEnabledRules(ruleFile, RuleTarget.FileName); + var files = EnumerateWordFiles(sourceDirectory); + foreach (var file in files) + { + var relativePath = PathUtility.GetRelativePath(sourceDirectory, file); + var relativeDirectory = Path.GetDirectoryName(relativePath) ?? string.Empty; + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); + var extension = Path.GetExtension(file); + var replacedFileNameWithoutExtension = ApplyRulesToFileName(fileNameWithoutExtension, rules); + ValidateReplacementFileName(file, replacedFileNameWithoutExtension); + + var datedFileName = string.Format("{0}-{1:yyyyMMdd}{2}", replacedFileNameWithoutExtension, DateTime.Today, extension); + var outputPath = hasOutputDirectory + ? Path.Combine(outputDirectory, relativeDirectory, datedFileName) + : string.Empty; + var diffPath = hasOutputDirectory + ? Path.Combine(outputDirectory, relativeDirectory, string.Format("{0}-{1:yyyyMMdd}.diff", replacedFileNameWithoutExtension, DateTime.Today)) + : string.Empty; + + items.Add(new FileScanItem + { + IsSelected = true, + SourcePath = file, + OutputPath = outputPath, + DiffPath = diffPath, + }); + } + + return items; + } + + public IList ScanRestoreCandidates(string replaceDirectory, string restoreDirectory, string diffInputFormat) + { + var hasRestoreDirectory = !string.IsNullOrWhiteSpace(restoreDirectory); + var normalizedFormat = string.Equals(diffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase) + ? ".bmp" + : ".diff"; + var selectedDiffs = Directory.Exists(replaceDirectory) + ? Directory.EnumerateFiles(replaceDirectory, "*" + normalizedFormat, SearchOption.AllDirectories).ToList() + : new List(); + var diffLookup = selectedDiffs.ToDictionary( + path => BuildRestoreKey(replaceDirectory, path), + StringComparer.OrdinalIgnoreCase); + + var wordFiles = EnumerateWordFiles(replaceDirectory); + var items = new List(); + foreach (var wordFile in wordFiles) + { + var key = BuildRestoreKey(replaceDirectory, wordFile); + if (!diffLookup.ContainsKey(key)) + { + continue; + } + + var relativePath = PathUtility.GetRelativePath(replaceDirectory, wordFile); + var relativeDirectory = Path.GetDirectoryName(relativePath) ?? string.Empty; + var extension = Path.GetExtension(wordFile); + var restoreFileName = ResolveRestoreFileName(diffLookup[key], wordFile); + var restorePath = hasRestoreDirectory + ? Path.Combine(restoreDirectory, relativeDirectory, restoreFileName ?? Path.GetFileNameWithoutExtension(wordFile) + "-rcv" + extension) + : string.Empty; + + items.Add(new FileScanItem + { + IsSelected = true, + SourcePath = wordFile, + OutputPath = restorePath, + DiffPath = diffLookup[key], + }); + } + + return items; + } + + public IList ValidateReplaceDirectories(string sourceDirectory, string outputDirectory) + { + return PathUtility.ValidateDirectoryPair("原始文件夹", sourceDirectory, "替换文件夹", outputDirectory).ToList(); + } + + public IList ValidateRestoreDirectories(string replaceDirectory, string restoreDirectory) + { + return PathUtility.ValidateDirectoryPair("替换文件夹", replaceDirectory, "恢复文件夹", restoreDirectory).ToList(); + } + + private static IEnumerable EnumerateWordFiles(string rootDirectory) + { + if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateFiles(rootDirectory, "*.*", SearchOption.AllDirectories) + .Where(path => + { + var fileName = Path.GetFileName(path); + if (fileName != null && fileName.StartsWith("~$", StringComparison.Ordinal)) + { + return false; + } + + var extension = Path.GetExtension(path); + return extension.Equals(".doc", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".docx", StringComparison.OrdinalIgnoreCase); + }) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private string ResolveRestoreFileName(string diffPath, string wordFile) + { + try + { + var document = _diffSerializationService.ReadMetadata(diffPath); + var sourceFileName = document == null || document.SourceDocument == null + ? null + : document.SourceDocument.FileName; + if (!string.IsNullOrWhiteSpace(sourceFileName)) + { + var safeFileName = Path.GetFileName(sourceFileName); + if (!string.IsNullOrWhiteSpace(safeFileName) + && string.Equals(safeFileName, sourceFileName, StringComparison.Ordinal) + && !ContainsInvalidFileNameCharacter(safeFileName)) + { + return safeFileName; + } + } + } + catch + { + // Legacy or placeholder diff files are still paired by basename and use the old preview naming. + } + + return Path.GetFileNameWithoutExtension(wordFile) + "-rcv" + Path.GetExtension(wordFile); + } + + private static IList GetEnabledRules(RuleFile ruleFile, RuleTarget target) + { + if (ruleFile == null || ruleFile.Rules == null) + { + return new List(); + } + + return ruleFile.Rules + .Where(rule => rule != null && rule.IsEnabled && !string.IsNullOrEmpty(rule.SearchText) && rule.Target == target) + .ToList(); + } + + public static string ApplyRulesToFileName(string fileNameWithoutExtension, IList rules) + { + var result = fileNameWithoutExtension ?? string.Empty; + if (rules == null || rules.Count == 0 || string.IsNullOrEmpty(result)) + { + return result; + } + + foreach (var rule in rules) + { + result = ApplyRuleToFileName(result, rule); + } + + return result; + } + + private static string ApplyRuleToFileName(string text, RuleDefinition rule) + { + if (string.IsNullOrEmpty(text) || rule == null || string.IsNullOrEmpty(rule.SearchText)) + { + return text; + } + + if (rule.MatchMode == RuleMatchMode.RegularExpression) + { + var regex = new Regex(rule.SearchText, GetRegexOptions(rule), RuleMatchTimeout); + return regex.Replace(text, match => + { + if (!match.Success || match.Length <= 0) + { + return match.Value; + } + + return match.Result(rule.ReplacementText ?? string.Empty); + }); + } + + var escapedPattern = Regex.Escape(rule.SearchText); + var plainRegex = new Regex(escapedPattern, GetRegexOptions(rule), RuleMatchTimeout); + return plainRegex.Replace(text, match => rule.ReplacementText ?? string.Empty); + } + + private static string ApplyRuleToText(string text, RuleDefinition rule) + { + if (string.IsNullOrEmpty(text) || rule == null || string.IsNullOrEmpty(rule.SearchText)) + { + return text; + } + + if (rule.MatchMode == RuleMatchMode.RegularExpression) + { + var regex = new Regex(rule.SearchText, GetRegexOptions(rule), RuleMatchTimeout); + return regex.Replace(text, match => + { + if (!match.Success || match.Length <= 0) + { + return match.Value; + } + + if (rule.IsWholeWord && !IsWholeWord(text, match.Index, match.Length)) + { + return match.Value; + } + + return match.Result(rule.ReplacementText ?? string.Empty); + }); + } + + var escapedPattern = Regex.Escape(rule.SearchText); + var plainRegex = new Regex(escapedPattern, GetRegexOptions(rule), RuleMatchTimeout); + return plainRegex.Replace(text, match => + { + if (rule.IsWholeWord && !IsWholeWord(text, match.Index, match.Length)) + { + return match.Value; + } + + return rule.ReplacementText ?? string.Empty; + }); + } + + private static RegexOptions GetRegexOptions(RuleDefinition rule) + { + var options = RegexOptions.CultureInvariant; + if (!rule.IsCaseSensitive) + { + options |= RegexOptions.IgnoreCase; + } + + return options; + } + + private static bool IsWholeWord(string text, int start, int length) + { + var leftOk = start <= 0 || GetWordCharClass(text[start - 1]) != GetWordCharClass(text[start]); + var rightIndex = start + length; + var rightOk = rightIndex >= text.Length || 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 void ValidateReplacementFileName(string sourcePath, string fileNameWithoutExtension) + { + if (string.IsNullOrWhiteSpace(fileNameWithoutExtension)) + { + throw new InvalidDataException("文件名规则替换结果为空,无法生成输出文件名。源文件: " + sourcePath); + } + + if (ContainsInvalidFileNameCharacter(fileNameWithoutExtension)) + { + throw new InvalidDataException("文件名规则替换结果包含非法文件名字符,无法生成输出文件名。源文件: " + + sourcePath + + ";替换后文件名: " + + fileNameWithoutExtension); + } + } + + private static bool ContainsInvalidFileNameCharacter(string fileName) + { + if (fileName == null) + { + return true; + } + + return fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 + || fileName.IndexOf(Path.DirectorySeparatorChar) >= 0 + || fileName.IndexOf(Path.AltDirectorySeparatorChar) >= 0; + } + + private static string BuildRestoreKey(string rootDirectory, string path) + { + var relativePath = PathUtility.GetRelativePath(rootDirectory, path); + var directory = Path.GetDirectoryName(relativePath) ?? string.Empty; + var fileName = Path.GetFileNameWithoutExtension(relativePath); + return Path.Combine(directory, fileName); + } + } +} diff --git a/DCIT.Core/Services/FontValidationService.cs b/DCIT.Core/Services/FontValidationService.cs new file mode 100644 index 0000000..a95ff0d --- /dev/null +++ b/DCIT.Core/Services/FontValidationService.cs @@ -0,0 +1,908 @@ +using System; +using System.Collections.Generic; +using System.Drawing.Text; +using System.IO; +using System.Linq; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using W = DocumentFormat.OpenXml.Wordprocessing; + +namespace DCIT.Core.Services +{ + public sealed class FontValidationService : IFontValidationService + { + private const string WordprocessingNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; + private const string DefaultEastAsiaFallbackFont = "宋体"; + private const string DefaultLatinFallbackFont = "Times New Roman"; + + private static readonly string[] ThemeFontTokens = + { + "majorAscii", + "majorHAnsi", + "majorEastAsia", + "majorBidi", + "minorAscii", + "minorHAnsi", + "minorEastAsia", + "minorBidi", + }; + + private static readonly string[][] KnownFontAliasGroups = + { + new[] { "宋体", "SimSun" }, + new[] { "新宋体", "NSimSun" }, + new[] { "黑体", "SimHei" }, + new[] { "仿宋", "FangSong" }, + new[] { "楷体", "KaiTi" }, + new[] { "微软雅黑", "Microsoft YaHei" }, + new[] { "微软雅黑 Light", "Microsoft YaHei Light" }, + new[] { "微软雅黑 Bold", "Microsoft YaHei Bold" }, + new[] { "等线", "DengXian" }, + new[] { "等线 Light", "DengXian Light" }, + new[] { "等线 Bold", "DengXian Bold" }, + new[] { "华文宋体", "STSong" }, + new[] { "华文黑体", "STHeiti" }, + new[] { "华文中宋", "STZhongsong" }, + new[] { "华文仿宋", "STFangsong" }, + new[] { "华文楷体", "STKaiti" }, + new[] { "幼圆", "YouYuan" }, + new[] { "隶书", "LiSu" }, + }; + + private static readonly Lazy>> KnownFontAliases = + new Lazy>>(BuildKnownFontAliases); + + private readonly Lazy> _installedFonts; + + public FontValidationService() + : this(null) + { + } + + public FontValidationService(IEnumerable installedFonts) + { + _installedFonts = new Lazy>( + delegate + { + return installedFonts == null + ? LoadInstalledFonts() + : new HashSet( + installedFonts + .Select(NormalizeFontName) + .Where(name => !string.IsNullOrWhiteSpace(name)), + StringComparer.OrdinalIgnoreCase); + }); + } + + public FontValidationResult Validate(string docxPath) + { + if (string.IsNullOrWhiteSpace(docxPath)) + { + throw new ArgumentException("docxPath"); + } + + if (!File.Exists(docxPath)) + { + throw new FileNotFoundException("未找到待校验的文档。", docxPath); + } + + using (var document = WordprocessingDocument.Open(docxPath, false)) + { + var themeFonts = BuildThemeFontMap(document); + var styleIndex = BuildStyleIndex(document); + var defaultFonts = ReadDocumentDefaultFonts(document, themeFonts); + var documentFontAliases = BuildDocumentFontAliasMap(document); + var usedFonts = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var root in EnumerateRelevantRoots(document)) + { + CollectWordRunFonts(root, styleIndex, defaultFonts, themeFonts, usedFonts); + CollectDrawingFonts(root, usedFonts); + } + + var missingFonts = usedFonts + .Select(NormalizeFontName) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Where(name => !IsFontAvailable(name, documentFontAliases)) + .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + return new FontValidationResult( + usedFonts.OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToList(), + missingFonts); + } + } + + public FontValidationResult EnsureFontsAvailable(string docxPath) + { + var result = Validate(docxPath); + if (result.MissingFonts.Count == 0) + { + return result; + } + + ApplyMissingFontFallbacks(docxPath, result.MissingFonts); + return result; + } + + private static void ApplyMissingFontFallbacks(string docxPath, IEnumerable missingFonts) + { + var missingFontSet = new HashSet( + (missingFonts ?? Array.Empty()) + .Select(NormalizeFontName) + .Where(name => !string.IsNullOrWhiteSpace(name)), + StringComparer.OrdinalIgnoreCase); + if (missingFontSet.Count == 0) + { + return; + } + + using (var document = WordprocessingDocument.Open(docxPath, true)) + { + var themeFonts = BuildThemeFontMap(document); + foreach (var root in EnumerateFontFallbackRoots(document)) + { + var changed = false; + foreach (var runFonts in root.Descendants()) + { + changed |= ApplyWordRunFontFallbacks(runFonts, missingFontSet, themeFonts); + } + + foreach (var element in root.Descendants()) + { + changed |= ApplyDrawingFontFallback(element, missingFontSet); + } + + if (changed) + { + root.Save(); + } + } + } + } + + private static IEnumerable EnumerateFontFallbackRoots(WordprocessingDocument document) + { + var main = document.MainDocumentPart; + if (main == null) + { + yield break; + } + + if (main.Document != null) + { + yield return main.Document; + } + + var queue = new Queue(); + var visited = new HashSet(); + 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) + { + yield return part.RootElement; + } + + queue.Enqueue(part); + } + } + } + + private static bool ApplyWordRunFontFallbacks( + W.RunFonts runFonts, + ISet missingFonts, + IDictionary themeFonts) + { + var changed = false; + changed |= ApplyWordFontFallback( + runFonts, + "ascii", + new[] { "asciiTheme" }, + DefaultLatinFallbackFont, + missingFonts, + themeFonts); + changed |= ApplyWordFontFallback( + runFonts, + "hAnsi", + new[] { "hAnsiTheme" }, + DefaultLatinFallbackFont, + missingFonts, + themeFonts); + changed |= ApplyWordFontFallback( + runFonts, + "eastAsia", + new[] { "eastAsiaTheme" }, + DefaultEastAsiaFallbackFont, + missingFonts, + themeFonts); + changed |= ApplyWordFontFallback( + runFonts, + "cs", + new[] { "csTheme", "cstheme" }, + DefaultLatinFallbackFont, + missingFonts, + themeFonts); + return changed; + } + + private static bool ApplyWordFontFallback( + OpenXmlElement element, + string directLocalName, + IEnumerable themeLocalNames, + string fallbackFont, + ISet missingFonts, + IDictionary themeFonts) + { + var directFont = NormalizeFontName(GetAttributeValue(element, directLocalName)); + var resolvedFont = directFont; + if (string.IsNullOrWhiteSpace(resolvedFont)) + { + foreach (var themeLocalName in themeLocalNames) + { + resolvedFont = ResolveFontName(null, GetAttributeValue(element, themeLocalName), themeFonts); + if (!string.IsNullOrWhiteSpace(resolvedFont)) + { + break; + } + } + } + + if (string.IsNullOrWhiteSpace(resolvedFont) || !missingFonts.Contains(resolvedFont)) + { + return false; + } + + var changed = false; + if (!string.Equals(directFont, fallbackFont, StringComparison.OrdinalIgnoreCase)) + { + SetWordprocessingAttribute(element, directLocalName, fallbackFont); + changed = true; + } + + changed |= RemoveAttributesByLocalName(element, themeLocalNames); + return changed; + } + + private static bool ApplyDrawingFontFallback(OpenXmlElement element, ISet missingFonts) + { + if (!(string.Equals(element.LocalName, "latin", StringComparison.OrdinalIgnoreCase) + || string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase) + || string.Equals(element.LocalName, "cs", StringComparison.OrdinalIgnoreCase) + || string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + var typeface = NormalizeFontName(GetAttributeValue(element, "typeface")); + if (string.IsNullOrWhiteSpace(typeface) || !missingFonts.Contains(typeface)) + { + return false; + } + + var fallbackFont = ResolveDrawingFallbackFont(element); + if (string.Equals(typeface, fallbackFont, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + element.SetAttribute(new OpenXmlAttribute(string.Empty, "typeface", string.Empty, fallbackFont)); + return true; + } + + private static string ResolveDrawingFallbackFont(OpenXmlElement element) + { + if (string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase)) + { + return DefaultEastAsiaFallbackFont; + } + + if (string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase) + && IsEastAsiaScript(GetAttributeValue(element, "script"))) + { + return DefaultEastAsiaFallbackFont; + } + + return DefaultLatinFallbackFont; + } + + private static bool IsEastAsiaScript(string script) + { + var normalized = NormalizeFontName(script); + if (string.IsNullOrWhiteSpace(normalized)) + { + return false; + } + + return normalized.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0 + || normalized.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0 + || normalized.IndexOf("Jpan", StringComparison.OrdinalIgnoreCase) >= 0 + || normalized.IndexOf("Hang", StringComparison.OrdinalIgnoreCase) >= 0 + || normalized.IndexOf("Kore", StringComparison.OrdinalIgnoreCase) >= 0 + || normalized.IndexOf("Bopo", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static void SetWordprocessingAttribute(OpenXmlElement element, string localName, string value) + { + element.SetAttribute(new OpenXmlAttribute("w", localName, WordprocessingNamespace, value)); + } + + private static bool RemoveAttributesByLocalName(OpenXmlElement element, IEnumerable localNames) + { + var names = new HashSet( + (localNames ?? Array.Empty()).Where(name => !string.IsNullOrWhiteSpace(name)), + StringComparer.OrdinalIgnoreCase); + if (names.Count == 0) + { + return false; + } + + var changed = false; + foreach (var attribute in element.GetAttributes().Where(attribute => names.Contains(attribute.LocalName)).ToList()) + { + element.RemoveAttribute(attribute.LocalName, attribute.NamespaceUri); + changed = true; + } + + return changed; + } + + private static HashSet LoadInstalledFonts() + { + var collection = new InstalledFontCollection(); + return new HashSet( + collection.Families + .Select(item => NormalizeFontName(item.Name)) + .Where(name => !string.IsNullOrWhiteSpace(name)), + StringComparer.OrdinalIgnoreCase); + } + + private static Dictionary> BuildDocumentFontAliasMap(WordprocessingDocument document) + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var fontTable = document.MainDocumentPart == null || document.MainDocumentPart.FontTablePart == null + ? null + : document.MainDocumentPart.FontTablePart.Fonts; + if (fontTable == null) + { + return result; + } + + foreach (var font in fontTable.Elements()) + { + var primary = NormalizeFontName(font.Name == null ? null : font.Name.Value); + var alternate = NormalizeFontName(font.AltName == null || font.AltName.Val == null ? null : font.AltName.Val.Value); + AddAliasPair(result, primary, alternate); + } + + return result; + } + + private static IEnumerable EnumerateRelevantRoots(WordprocessingDocument document) + { + var main = document.MainDocumentPart; + if (main == null) + { + yield break; + } + + if (main.Document != null) + { + yield return main.Document; + } + + var queue = new Queue(); + var visited = new HashSet(); + 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 && IsRelevantPart(part)) + { + yield return part.RootElement; + } + + queue.Enqueue(part); + } + } + } + + private static bool IsRelevantPart(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 Dictionary BuildStyleIndex(WordprocessingDocument document) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var styles = document.MainDocumentPart == null || document.MainDocumentPart.StyleDefinitionsPart == null + ? null + : document.MainDocumentPart.StyleDefinitionsPart.Styles; + + if (styles == null) + { + return result; + } + + foreach (var style in styles.Elements()) + { + var styleId = style.StyleId == null ? null : style.StyleId.Value; + if (string.IsNullOrWhiteSpace(styleId)) + { + continue; + } + + result[styleId] = new StyleFontSet + { + StyleId = styleId, + BasedOnStyleId = style.BasedOn == null ? null : style.BasedOn.Val == null ? null : style.BasedOn.Val.Value, + Fonts = ExtractWordFontSet(style.StyleRunProperties == null ? null : style.StyleRunProperties.RunFonts, null), + }; + } + + return result; + } + + private static FontReferenceSet ReadDocumentDefaultFonts(WordprocessingDocument document, IDictionary themeFonts) + { + var styles = document.MainDocumentPart == null || document.MainDocumentPart.StyleDefinitionsPart == null + ? null + : document.MainDocumentPart.StyleDefinitionsPart.Styles; + var docDefaults = styles == null ? null : styles.DocDefaults; + var runDefaults = docDefaults == null ? null : docDefaults.RunPropertiesDefault; + var baseStyle = runDefaults == null ? null : runDefaults.RunPropertiesBaseStyle; + return ExtractWordFontSet(baseStyle == null ? null : baseStyle.RunFonts, themeFonts); + } + + private static IDictionary BuildThemeFontMap(WordprocessingDocument document) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var themePart = document.MainDocumentPart == null ? null : document.MainDocumentPart.ThemePart; + var theme = themePart == null ? null : themePart.Theme; + if (theme == null) + { + return result; + } + + var majorFont = theme.Descendants().FirstOrDefault(item => string.Equals(item.LocalName, "majorFont", StringComparison.OrdinalIgnoreCase)); + var minorFont = theme.Descendants().FirstOrDefault(item => string.Equals(item.LocalName, "minorFont", StringComparison.OrdinalIgnoreCase)); + + AddThemeSlot(result, "majorAscii", majorFont, "latin"); + AddThemeSlot(result, "majorHAnsi", majorFont, "latin"); + AddThemeSlot(result, "majorEastAsia", majorFont, "ea"); + AddThemeSlot(result, "majorBidi", majorFont, "cs"); + AddThemeSlot(result, "minorAscii", minorFont, "latin"); + AddThemeSlot(result, "minorHAnsi", minorFont, "latin"); + AddThemeSlot(result, "minorEastAsia", minorFont, "ea"); + AddThemeSlot(result, "minorBidi", minorFont, "cs"); + + return result; + } + + private static void AddThemeSlot(IDictionary themeFonts, string token, OpenXmlElement schemeRoot, string localName) + { + if (schemeRoot == null) + { + return; + } + + var element = schemeRoot.Elements().FirstOrDefault(item => string.Equals(item.LocalName, localName, StringComparison.OrdinalIgnoreCase)); + if (element == null) + { + return; + } + + var typeface = GetAttributeValue(element, "typeface"); + var normalized = NormalizeFontName(typeface); + if (!string.IsNullOrWhiteSpace(normalized)) + { + themeFonts[token] = normalized; + } + } + + private static void CollectWordRunFonts( + OpenXmlElement root, + IDictionary styleIndex, + FontReferenceSet defaultFonts, + IDictionary themeFonts, + ISet usedFonts) + { + foreach (var run in root.Descendants()) + { + if (!ContainsVisibleText(run)) + { + continue; + } + + var effectiveFonts = ResolveEffectiveWordFonts(run, styleIndex, defaultFonts, themeFonts); + foreach (var font in effectiveFonts.GetDefinedFonts()) + { + usedFonts.Add(font); + } + } + } + + private static void CollectDrawingFonts(OpenXmlElement root, ISet usedFonts) + { + foreach (var element in root.Descendants()) + { + if (!(string.Equals(element.LocalName, "latin", StringComparison.OrdinalIgnoreCase) + || string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase) + || string.Equals(element.LocalName, "cs", StringComparison.OrdinalIgnoreCase) + || string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + var typeface = NormalizeFontName(GetAttributeValue(element, "typeface")); + if (!string.IsNullOrWhiteSpace(typeface)) + { + usedFonts.Add(typeface); + } + } + } + + private static bool ContainsVisibleText(W.Run run) + { + if (run == null || string.IsNullOrWhiteSpace(run.InnerText)) + { + return false; + } + + if (run.Ancestors().Any()) + { + return false; + } + + return run.RunProperties == null || run.RunProperties.Vanish == null; + } + + private static FontReferenceSet ResolveEffectiveWordFonts( + W.Run run, + IDictionary styleIndex, + FontReferenceSet defaultFonts, + IDictionary themeFonts) + { + var effectiveFonts = new FontReferenceSet(); + effectiveFonts.ApplyOverride(defaultFonts); + + var paragraph = run.Ancestors().FirstOrDefault(); + if (paragraph != null && paragraph.ParagraphProperties != null) + { + var paragraphStyleId = paragraph.ParagraphProperties.ParagraphStyleId == null + ? null + : paragraph.ParagraphProperties.ParagraphStyleId.Val == null + ? null + : paragraph.ParagraphProperties.ParagraphStyleId.Val.Value; + effectiveFonts.ApplyOverride(ResolveStyleFonts(paragraphStyleId, styleIndex)); + } + + var table = run.Ancestors().FirstOrDefault(); + if (table != null && table.TableProperties != null && table.TableProperties.TableStyle != null) + { + var tableStyleId = table.TableProperties.TableStyle.Val == null ? null : table.TableProperties.TableStyle.Val.Value; + effectiveFonts.ApplyOverride(ResolveStyleFonts(tableStyleId, styleIndex)); + } + + if (run.RunProperties != null && run.RunProperties.RunStyle != null) + { + var runStyleId = run.RunProperties.RunStyle.Val == null ? null : run.RunProperties.RunStyle.Val.Value; + effectiveFonts.ApplyOverride(ResolveStyleFonts(runStyleId, styleIndex)); + } + + effectiveFonts.ApplyOverride(ExtractWordFontSet(run.RunProperties == null ? null : run.RunProperties.RunFonts, themeFonts)); + return effectiveFonts; + } + + private static FontReferenceSet ResolveStyleFonts(string styleId, IDictionary styleIndex) + { + var result = new FontReferenceSet(); + if (string.IsNullOrWhiteSpace(styleId) || styleIndex == null || !styleIndex.TryGetValue(styleId, out var style)) + { + return result; + } + + var resolved = ResolveStyleFonts(styleId, styleIndex, new HashSet(StringComparer.OrdinalIgnoreCase)); + result.ApplyOverride(resolved); + return result; + } + + private static FontReferenceSet ResolveStyleFonts( + string styleId, + IDictionary styleIndex, + ISet visited) + { + var result = new FontReferenceSet(); + if (string.IsNullOrWhiteSpace(styleId) + || styleIndex == null + || !styleIndex.TryGetValue(styleId, out var style) + || !visited.Add(styleId)) + { + return result; + } + + result.ApplyOverride(ResolveStyleFonts(style.BasedOnStyleId, styleIndex, visited)); + result.ApplyOverride(style.Fonts); + return result; + } + + private static FontReferenceSet ExtractWordFontSet(W.RunFonts runFonts, IDictionary themeFonts) + { + var result = new FontReferenceSet(); + if (runFonts == null) + { + return result; + } + + result.Ascii = ResolveFontName(GetAttributeValue(runFonts, "ascii"), GetAttributeValue(runFonts, "asciiTheme"), themeFonts); + result.HighAnsi = ResolveFontName(GetAttributeValue(runFonts, "hAnsi"), GetAttributeValue(runFonts, "hAnsiTheme"), themeFonts); + result.EastAsia = ResolveFontName(GetAttributeValue(runFonts, "eastAsia"), GetAttributeValue(runFonts, "eastAsiaTheme"), themeFonts); + result.ComplexScript = ResolveFontName(GetAttributeValue(runFonts, "cs"), GetAttributeValue(runFonts, "cstheme"), themeFonts); + if (string.IsNullOrWhiteSpace(result.ComplexScript)) + { + result.ComplexScript = ResolveFontName(null, GetAttributeValue(runFonts, "csTheme"), themeFonts); + } + + return result; + } + + private static string ResolveFontName(string directValue, string themeToken, IDictionary themeFonts) + { + var normalizedDirect = NormalizeFontName(directValue); + if (!string.IsNullOrWhiteSpace(normalizedDirect)) + { + return normalizedDirect; + } + + var normalizedToken = NormalizeFontName(themeToken); + if (!string.IsNullOrWhiteSpace(normalizedToken) + && themeFonts != null + && themeFonts.TryGetValue(normalizedToken, out var themeFont)) + { + return NormalizeFontName(themeFont); + } + + return null; + } + + private static string NormalizeFontName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var normalized = value.Trim().Trim('"', '\''); + if (normalized.StartsWith("@", StringComparison.Ordinal)) + { + normalized = normalized.Substring(1); + } + + return normalized.Length == 0 ? null : normalized; + } + + private static string GetAttributeValue(OpenXmlElement element, string localName) + { + if (element == null || string.IsNullOrWhiteSpace(localName)) + { + return null; + } + + foreach (var attribute in element.GetAttributes()) + { + if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase)) + { + return attribute.Value; + } + } + + return null; + } + + private bool IsFontAvailable(string fontName, IDictionary> documentFontAliases) + { + if (string.IsNullOrWhiteSpace(fontName)) + { + return true; + } + + var installedFonts = _installedFonts.Value; + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var queue = new Queue(); + queue.Enqueue(fontName); + + while (queue.Count > 0) + { + var current = NormalizeFontName(queue.Dequeue()); + if (string.IsNullOrWhiteSpace(current) || !visited.Add(current)) + { + continue; + } + + if (installedFonts.Contains(current)) + { + return true; + } + + EnqueueAliases(current, documentFontAliases, queue); + EnqueueAliases(current, KnownFontAliases.Value, queue); + } + + return false; + } + + private static Dictionary> BuildKnownFontAliases() + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var group in KnownFontAliasGroups) + { + if (group == null || group.Length < 2) + { + continue; + } + + for (var index = 0; index < group.Length; index++) + { + var current = NormalizeFontName(group[index]); + if (string.IsNullOrWhiteSpace(current)) + { + continue; + } + + for (var inner = 0; inner < group.Length; inner++) + { + if (inner == index) + { + continue; + } + + AddAliasPair(result, current, group[inner]); + } + } + } + + return result; + } + + private static void EnqueueAliases(string fontName, IDictionary> aliases, Queue queue) + { + if (queue == null || aliases == null || string.IsNullOrWhiteSpace(fontName)) + { + return; + } + + if (!aliases.TryGetValue(fontName, out var values) || values == null) + { + return; + } + + foreach (var value in values) + { + var normalized = NormalizeFontName(value); + if (!string.IsNullOrWhiteSpace(normalized)) + { + queue.Enqueue(normalized); + } + } + } + + private static void AddAliasPair(IDictionary> aliases, string left, string right) + { + var normalizedLeft = NormalizeFontName(left); + var normalizedRight = NormalizeFontName(right); + if (string.IsNullOrWhiteSpace(normalizedLeft) + || string.IsNullOrWhiteSpace(normalizedRight) + || string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + AddAliasValue(aliases, normalizedLeft, normalizedRight); + AddAliasValue(aliases, normalizedRight, normalizedLeft); + } + + private static void AddAliasValue(IDictionary> aliases, string key, string value) + { + if (!aliases.TryGetValue(key, out var values)) + { + values = new HashSet(StringComparer.OrdinalIgnoreCase); + aliases[key] = values; + } + + values.Add(value); + } + + private sealed class StyleFontSet + { + public string StyleId { get; set; } + + public string BasedOnStyleId { get; set; } + + public FontReferenceSet Fonts { get; set; } + } + } + + public sealed class FontValidationResult + { + public FontValidationResult(IList usedFonts, IList missingFonts) + { + UsedFonts = usedFonts ?? Array.Empty(); + MissingFonts = missingFonts ?? Array.Empty(); + } + + public IList UsedFonts { get; private set; } + + public IList MissingFonts { get; private set; } + } + + internal sealed class FontReferenceSet + { + public string Ascii { get; set; } + + public string HighAnsi { get; set; } + + public string EastAsia { get; set; } + + public string ComplexScript { get; set; } + + public IEnumerable GetDefinedFonts() + { + return new[] { Ascii, HighAnsi, EastAsia, ComplexScript } + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.OrdinalIgnoreCase); + } + + public void ApplyOverride(FontReferenceSet other) + { + if (other == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(other.Ascii)) + { + Ascii = other.Ascii; + } + + if (!string.IsNullOrWhiteSpace(other.HighAnsi)) + { + HighAnsi = other.HighAnsi; + } + + if (!string.IsNullOrWhiteSpace(other.EastAsia)) + { + EastAsia = other.EastAsia; + } + + if (!string.IsNullOrWhiteSpace(other.ComplexScript)) + { + ComplexScript = other.ComplexScript; + } + } + } +} diff --git a/DCIT.Core/Services/IDocConversionService.cs b/DCIT.Core/Services/IDocConversionService.cs new file mode 100644 index 0000000..c824461 --- /dev/null +++ b/DCIT.Core/Services/IDocConversionService.cs @@ -0,0 +1,7 @@ +namespace DCIT.Core.Services +{ + public interface IDocConversionService + { + void Convert(string sourcePath, string outputPath); + } +} diff --git a/DCIT.Core/Services/IDocumentFieldUpdateService.cs b/DCIT.Core/Services/IDocumentFieldUpdateService.cs new file mode 100644 index 0000000..42fb6e7 --- /dev/null +++ b/DCIT.Core/Services/IDocumentFieldUpdateService.cs @@ -0,0 +1,7 @@ +namespace DCIT.Core.Services +{ + public interface IDocumentFieldUpdateService + { + void UpdateDynamicContent(string documentPath); + } +} diff --git a/DCIT.Core/Services/IDocumentProcessingService.cs b/DCIT.Core/Services/IDocumentProcessingService.cs new file mode 100644 index 0000000..5c96c7e --- /dev/null +++ b/DCIT.Core/Services/IDocumentProcessingService.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading; +using DCIT.Core.Models; + +namespace DCIT.Core.Services +{ + public interface IDocumentProcessingService + { + FileProcessResult Replace(ReplaceFileRequest request, IProgress progress, CancellationToken cancellationToken); + + FileProcessResult Restore(RestoreFileRequest request, IProgress progress, CancellationToken cancellationToken); + } +} diff --git a/DCIT.Core/Services/IFontValidationService.cs b/DCIT.Core/Services/IFontValidationService.cs new file mode 100644 index 0000000..9d21593 --- /dev/null +++ b/DCIT.Core/Services/IFontValidationService.cs @@ -0,0 +1,9 @@ +namespace DCIT.Core.Services +{ + public interface IFontValidationService + { + FontValidationResult Validate(string docxPath); + + FontValidationResult EnsureFontsAvailable(string docxPath); + } +} diff --git a/DCIT.Core/Services/LogService.cs b/DCIT.Core/Services/LogService.cs new file mode 100644 index 0000000..0ad6408 --- /dev/null +++ b/DCIT.Core/Services/LogService.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Text; +using DCIT.Core.Models; + +namespace DCIT.Core.Services +{ + public class LogService + { + private readonly object _syncRoot = new object(); + private readonly string _logFilePath; + private bool _runtimeFailureActive; + + public LogService(string startupDirectory) + { + _logFilePath = Path.Combine(startupDirectory, string.Format("session-{0:yyyyMMdd}.log", DateTime.Today)); + EnsureWritable(); + } + + public event EventHandler EntryWritten; + + public event EventHandler WriteFailed; + + public string LogFilePath + { + get { return _logFilePath; } + } + + public bool HasRuntimeFailure { get; private set; } + + public Exception LastRuntimeFailure { get; private set; } + + public void Info(string eventCode, string message, string detail = null) + { + Write(LogLevel.Info, eventCode, message, detail); + } + + public void Warning(string eventCode, string message, string detail = null) + { + Write(LogLevel.Warning, eventCode, message, detail); + } + + public void Error(string eventCode, string message, string detail = null) + { + Write(LogLevel.Error, eventCode, message, detail); + } + + public void Write(LogLevel level, string eventCode, string message, string detail = null) + { + var entry = new LogEntry + { + Level = level, + EventCode = eventCode, + Message = message, + Detail = detail, + }; + + var line = string.Format( + "{0:yyyy-MM-dd HH:mm:ss.fff}\t{1}\t{2}\t{3}", + entry.Timestamp, + level.ToString().ToUpperInvariant(), + entry.EventCode ?? string.Empty, + entry.Message ?? string.Empty); + + if (!string.IsNullOrWhiteSpace(entry.Detail)) + { + line += "\t" + entry.Detail.Replace("\r", " ").Replace("\n", " "); + } + + try + { + lock (_syncRoot) + { + File.AppendAllText(_logFilePath, line + Environment.NewLine, new UTF8Encoding(false)); + _runtimeFailureActive = false; + HasRuntimeFailure = false; + LastRuntimeFailure = null; + } + + EntryWritten?.Invoke(this, entry); + } + catch (Exception ex) + { + var shouldRaiseEvent = false; + lock (_syncRoot) + { + HasRuntimeFailure = true; + LastRuntimeFailure = ex; + if (!_runtimeFailureActive) + { + _runtimeFailureActive = true; + shouldRaiseEvent = true; + } + } + + if (shouldRaiseEvent) + { + WriteFailed?.Invoke(this, new LogWriteFailureEventArgs(entry, _logFilePath, ex)); + } + } + } + + private void EnsureWritable() + { + var directory = Path.GetDirectoryName(_logFilePath); + Directory.CreateDirectory(directory); + File.AppendAllText(_logFilePath, string.Empty, new UTF8Encoding(false)); + } + } +} diff --git a/DCIT.Core/Services/OfficeDocConversionService.cs b/DCIT.Core/Services/OfficeDocConversionService.cs new file mode 100644 index 0000000..81dcaf4 --- /dev/null +++ b/DCIT.Core/Services/OfficeDocConversionService.cs @@ -0,0 +1,518 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace DCIT.Core.Services +{ + public class OfficeDocConversionService : IDocConversionService, IDocumentFieldUpdateService + { + private const int WdFormatDocument97 = 0; + private const int WdFormatDocumentDefault = 16; + private static readonly string[] WpsProgIds = + { + "KWPS.Application", + "kwps.Application", + "WPS.Application", + "wps.Application", + }; + + private readonly string _applicationBaseDirectory; + + public OfficeDocConversionService(string applicationBaseDirectory = null) + { + _applicationBaseDirectory = string.IsNullOrWhiteSpace(applicationBaseDirectory) + ? AppContext.BaseDirectory + : applicationBaseDirectory; + } + + public void Convert(string sourcePath, string outputPath) + { + if (string.IsNullOrWhiteSpace(sourcePath)) + { + throw new ArgumentNullException("sourcePath"); + } + + if (string.IsNullOrWhiteSpace(outputPath)) + { + throw new ArgumentNullException("outputPath"); + } + + var sourceExtension = NormalizeExtension(sourcePath); + var outputExtension = NormalizeExtension(outputPath); + if (!IsSupportedConversion(sourceExtension, outputExtension)) + { + throw new NotSupportedException("仅支持 .doc 与 .docx 之间互相转换。"); + } + + var outputDirectory = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrWhiteSpace(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + } + + if (File.Exists(outputPath)) + { + File.Delete(outputPath); + } + + var errors = new List(); + foreach (var progId in GetOfficeAutomationProgIds()) + { + try + { + ConvertWithOfficeAutomationBackend(progId, sourcePath, outputPath, outputExtension); + return; + } + catch (Exception ex) + { + errors.Add(progId + ": " + ex.Message); + } + } + + var docToPath = ResolveDocToPath(); + if (!string.IsNullOrWhiteSpace(docToPath)) + { + try + { + ConvertWithDocToBackend(docToPath, sourcePath, outputPath, outputExtension); + return; + } + catch (Exception ex) + { + errors.Add("DocTo: " + ex.Message); + } + } + + throw new InvalidOperationException( + "未找到可用的 .doc 转换链路。请确认已安装可自动化的 Microsoft Word 或 WPS;如自动化不可用,可提供 docto.exe 作为兜底转换工具。" + + Environment.NewLine + + string.Join(Environment.NewLine, errors.Where(item => !string.IsNullOrWhiteSpace(item)))); + } + + public void UpdateDynamicContent(string documentPath) + { + if (string.IsNullOrWhiteSpace(documentPath)) + { + throw new ArgumentNullException("documentPath"); + } + + if (!File.Exists(documentPath)) + { + throw new FileNotFoundException("要更新动态内容的文档不存在。", documentPath); + } + + var extension = NormalizeExtension(documentPath); + if (!string.Equals(extension, ".docx", StringComparison.OrdinalIgnoreCase) + && !string.Equals(extension, ".doc", StringComparison.OrdinalIgnoreCase)) + { + throw new NotSupportedException("动态内容更新仅支持 .doc 或 .docx 文档。"); + } + + var errors = new List(); + foreach (var progId in GetOfficeAutomationProgIds()) + { + try + { + UpdateDynamicContentWithOfficeAutomationBackend(progId, documentPath); + return; + } + catch (Exception ex) + { + errors.Add(progId + ": " + ex.Message); + } + } + + throw new InvalidOperationException( + "未找到可用的目录/域刷新链路。请确保已安装可自动化的 Microsoft Word 或 WPS。" + + Environment.NewLine + + string.Join(Environment.NewLine, errors.Where(item => !string.IsNullOrWhiteSpace(item)))); + } + + protected virtual IEnumerable GetOfficeAutomationProgIds() + { + yield return "Word.Application"; + foreach (var progId in WpsProgIds) + { + yield return progId; + } + } + + protected virtual string ResolveDocToPath() + { + var candidates = new List(); + var envPath = Environment.GetEnvironmentVariable("DCIT_DOCTO_PATH"); + if (!string.IsNullOrWhiteSpace(envPath)) + { + candidates.Add(envPath); + } + + candidates.Add(Path.Combine(_applicationBaseDirectory, "docto.exe")); + candidates.Add(Path.Combine(_applicationBaseDirectory, "tools", "DocTo", "docto.exe")); + + foreach (var candidate in candidates) + { + if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate)) + { + return candidate; + } + } + + var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + foreach (var segment in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + try + { + var candidate = Path.Combine(segment.Trim(), "docto.exe"); + if (File.Exists(candidate)) + { + return candidate; + } + } + catch + { + } + } + + return null; + } + + protected virtual void ConvertWithDocToBackend(string docToPath, string sourcePath, string outputPath, string outputExtension) + { + var format = string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase) + ? "wdFormatDocumentDefault" + : "wdFormatDocument97"; + + var startInfo = new ProcessStartInfo + { + FileName = docToPath, + Arguments = string.Format("-F \"{0}\" -O \"{1}\" -T {2}", sourcePath, outputPath, format), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = Path.GetDirectoryName(docToPath) ?? _applicationBaseDirectory, + }; + + using (var process = Process.Start(startInfo)) + { + if (process == null) + { + throw new InvalidOperationException("无法启动 docto.exe。"); + } + + var standardOutput = process.StandardOutput.ReadToEnd(); + var standardError = process.StandardError.ReadToEnd(); + process.WaitForExit(120000); + + if (process.ExitCode != 0 || !File.Exists(outputPath)) + { + throw new InvalidOperationException( + "docto.exe 转换失败。" + + Environment.NewLine + + (string.IsNullOrWhiteSpace(standardOutput) ? string.Empty : standardOutput + Environment.NewLine) + + standardError); + } + } + } + + protected virtual void ConvertWithOfficeAutomationBackend(string progId, string sourcePath, string outputPath, string outputExtension) + { + ConvertWithWordAutomation(progId, sourcePath, outputPath, outputExtension); + } + + protected virtual void UpdateDynamicContentWithOfficeAutomationBackend(string progId, string documentPath) + { + UpdateDynamicContentWithWordAutomation(progId, documentPath); + } + + private static bool IsSupportedConversion(string sourceExtension, string outputExtension) + { + if (string.Equals(sourceExtension, outputExtension, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return (string.Equals(sourceExtension, ".doc", StringComparison.OrdinalIgnoreCase) + || string.Equals(sourceExtension, ".docx", StringComparison.OrdinalIgnoreCase)) + && (string.Equals(outputExtension, ".doc", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase)); + } + + private static string NormalizeExtension(string path) + { + return Path.GetExtension(path) ?? string.Empty; + } + + private static void ConvertWithWordAutomation(string progId, string sourcePath, string outputPath, string outputExtension) + { + var appType = Type.GetTypeFromProgID(progId, false); + if (appType == null) + { + throw new InvalidOperationException("未注册 COM ProgID: " + progId); + } + + object application = null; + object documents = null; + object document = null; + + try + { + application = Activator.CreateInstance(appType); + if (application == null) + { + throw new InvalidOperationException("无法创建 COM 实例: " + progId); + } + + TrySetProperty(application, "Visible", false); + TrySetProperty(application, "DisplayAlerts", 0); + documents = GetProperty(application, "Documents"); + if (documents == null) + { + throw new InvalidOperationException("无法获取 Documents 集合。"); + } + + document = InvokeMethod(documents, "Open", sourcePath, false, true); + if (document == null) + { + throw new InvalidOperationException("无法打开文档。"); + } + + var fileFormat = string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase) + ? WdFormatDocumentDefault + : WdFormatDocument97; + + try + { + InvokeMethod(document, "SaveAs2", outputPath, fileFormat); + } + catch (MissingMethodException) + { + InvokeMethod(document, "SaveAs", outputPath, fileFormat); + } + + if (!File.Exists(outputPath)) + { + throw new InvalidOperationException("Office 自动化未生成目标文件。"); + } + } + finally + { + SafeCloseDocument(document); + SafeQuitApplication(application); + ReleaseComObject(document); + ReleaseComObject(documents); + ReleaseComObject(application); + } + } + + private static void UpdateDynamicContentWithWordAutomation(string progId, string documentPath) + { + var appType = Type.GetTypeFromProgID(progId, false); + if (appType == null) + { + throw new InvalidOperationException("未注册 COM ProgID: " + progId); + } + + object application = null; + object documents = null; + object document = null; + + try + { + application = Activator.CreateInstance(appType); + if (application == null) + { + throw new InvalidOperationException("无法创建 COM 实例: " + progId); + } + + TrySetProperty(application, "Visible", false); + TrySetProperty(application, "DisplayAlerts", 0); + documents = GetProperty(application, "Documents"); + if (documents == null) + { + throw new InvalidOperationException("无法获取 Documents 集合。"); + } + + document = InvokeMethod(documents, "Open", documentPath, false, false); + if (document == null) + { + throw new InvalidOperationException("无法打开文档。"); + } + + TryInvokeMethod(document, "Activate"); + TryInvokeMethod(document, "Repaginate"); + TryUpdateCollectionItems(document, "TablesOfContents", "Update", "UpdatePageNumbers"); + TryUpdateCollectionItems(document, "TablesOfFigures", "Update"); + TryUpdateCollectionItems(document, "TablesOfAuthorities", "Update"); + TryUpdateCollection(document, "Fields", "Update"); + TryUpdateCollection(document, "Indexes", "Update"); + TryInvokeMethod(document, "Repaginate"); + InvokeMethod(document, "Save"); + } + finally + { + SafeCloseDocument(document); + SafeQuitApplication(application); + ReleaseComObject(document); + ReleaseComObject(documents); + ReleaseComObject(application); + } + } + + private static object GetProperty(object instance, string propertyName) + { + return instance.GetType().InvokeMember( + propertyName, + System.Reflection.BindingFlags.GetProperty, + null, + instance, + null); + } + + private static void TrySetProperty(object instance, string propertyName, object value) + { + try + { + instance.GetType().InvokeMember( + propertyName, + System.Reflection.BindingFlags.SetProperty, + null, + instance, + new[] { value }); + } + catch + { + } + } + + private static object InvokeMethod(object instance, string methodName, params object[] arguments) + { + return instance.GetType().InvokeMember( + methodName, + System.Reflection.BindingFlags.InvokeMethod, + null, + instance, + arguments); + } + + private static bool TryInvokeMethod(object instance, string methodName, params object[] arguments) + { + try + { + InvokeMethod(instance, methodName, arguments); + return true; + } + catch + { + return false; + } + } + + private static void TryUpdateCollection(object instance, string propertyName, string updateMethodName) + { + object collection = null; + try + { + collection = GetProperty(instance, propertyName); + if (collection == null) + { + return; + } + + TryInvokeMethod(collection, updateMethodName); + } + finally + { + ReleaseComObject(collection); + } + } + + private static void TryUpdateCollectionItems(object instance, string propertyName, params string[] methodNames) + { + object collection = null; + try + { + collection = GetProperty(instance, propertyName); + if (collection == null) + { + return; + } + + var countObject = GetProperty(collection, "Count"); + var count = System.Convert.ToInt32(countObject); + for (var index = 1; index <= count; index++) + { + object item = null; + try + { + item = InvokeMethod(collection, "Item", index); + foreach (var methodName in methodNames.Where(name => !string.IsNullOrWhiteSpace(name))) + { + TryInvokeMethod(item, methodName); + } + } + finally + { + ReleaseComObject(item); + } + } + } + catch + { + } + finally + { + ReleaseComObject(collection); + } + } + + private static void SafeCloseDocument(object document) + { + if (document == null) + { + return; + } + + try + { + InvokeMethod(document, "Close", false); + } + catch + { + } + } + + private static void SafeQuitApplication(object application) + { + if (application == null) + { + return; + } + + try + { + InvokeMethod(application, "Quit", false); + } + catch + { + } + } + + private static void ReleaseComObject(object instance) + { + try + { + if (instance != null && Marshal.IsComObject(instance)) + { + Marshal.FinalReleaseComObject(instance); + } + } + catch + { + } + } + } +} \ No newline at end of file diff --git a/DCIT.Core/Services/RuleService.cs b/DCIT.Core/Services/RuleService.cs new file mode 100644 index 0000000..5d7c2ef --- /dev/null +++ b/DCIT.Core/Services/RuleService.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using DCIT.Core.Models; +using Newtonsoft.Json; + +namespace DCIT.Core.Services +{ + public class RuleService + { + private static readonly TimeSpan RegexValidationTimeout = TimeSpan.FromMilliseconds(250); + + public RuleFile Load(string ruleFilePath) + { + if (string.IsNullOrWhiteSpace(ruleFilePath) || !System.IO.File.Exists(ruleFilePath)) + { + return new RuleFile(); + } + + var content = System.IO.File.ReadAllText(ruleFilePath, Encoding.UTF8); + return JsonConvert.DeserializeObject(content) ?? new RuleFile(); + } + + public void Save(string ruleFilePath, RuleFile ruleFile) + { + var report = Validate(ruleFile); + if (report.HasErrors) + { + var detail = string.Join(Environment.NewLine, report.Issues.Select(FormatIssue)); + throw new InvalidOperationException(detail); + } + + var content = JsonConvert.SerializeObject(ruleFile, Formatting.Indented); + System.IO.File.WriteAllText(ruleFilePath, content, new UTF8Encoding(false)); + } + + public RuleValidationReport Validate(RuleFile ruleFile) + { + ruleFile = ruleFile ?? new RuleFile(); + + var report = new RuleValidationReport(); + var idSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var validatedRules = new List(); + + for (var index = 0; index < ruleFile.Rules.Count; index++) + { + var rule = ruleFile.Rules[index] ?? new RuleDefinition(); + if (string.IsNullOrWhiteSpace(rule.Id)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "规则 ID 不能为空。")); + } + else if (!idSet.Add(rule.Id.Trim())) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "规则 ID 重复。")); + } + + if (string.IsNullOrWhiteSpace(rule.Name)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "规则名称为空。")); + } + + if (string.IsNullOrWhiteSpace(rule.SearchText)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "原文本或表达式不能为空。")); + continue; + } + + Regex compiledRegex = null; + if (rule.MatchMode == RuleMatchMode.RegularExpression) + { + try + { + compiledRegex = new Regex(rule.SearchText, GetRegexOptions(rule), RegexValidationTimeout); + ValidateRegexRule(report, index, rule, compiledRegex); + } + catch (Exception ex) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "正则表达式非法: " + ex.Message)); + } + } + + if (rule.MatchMode == RuleMatchMode.PlainText + && !string.IsNullOrEmpty(rule.ReplacementText) + && rule.ReplacementText.IndexOf(rule.SearchText, rule.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) >= 0) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "替换文本会再次命中原文本,请确认是否符合预期。")); + } + + if (rule.MatchMode == RuleMatchMode.RegularExpression + && compiledRegex != null + && !string.IsNullOrEmpty(rule.ReplacementText) + && IsRegexReplacementSelfMatching(compiledRegex, rule.ReplacementText)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "替换模板会再次命中当前规则,请确认是否符合预期。")); + } + + var current = new ValidatedRuleContext(index, rule, compiledRegex); + foreach (var previous in validatedRules) + { + AddCompatibilityIssues(report, previous, current); + } + + validatedRules.Add(current); + } + + return report; + } + + public RuleDefinition CreateAutoExtractRule(ExtractionCandidate candidate, IList existingRules) + { + var prefix = GetPrefix(candidate.PrimaryCategory); + var namePrefix = GetNamePrefix(candidate.PrimaryCategory); + var nextIndex = 1; + var existingIds = new HashSet(existingRules.Select(rule => rule.Id ?? string.Empty), StringComparer.OrdinalIgnoreCase); + var existingNames = new HashSet(existingRules.Select(rule => rule.Name ?? string.Empty), StringComparer.OrdinalIgnoreCase); + + string id; + do + { + id = string.Format("{0}{1:0000}", prefix, nextIndex++); + } + while (existingIds.Contains(id)); + + nextIndex = 1; + string name; + do + { + name = string.Format("{0}-{1:0000}", namePrefix, nextIndex++); + } + while (existingNames.Contains(name)); + + return new RuleDefinition + { + Id = id, + Name = name, + IsEnabled = true, + MatchMode = RuleMatchMode.PlainText, + IsCaseSensitive = false, + IsWholeWord = false, + SearchText = candidate.OriginalText, + ReplacementText = string.Empty, + Note = string.Format( + "自动提取; 类型={0}; 统计={1}; 首次文档={2}; 时间={3:yyyy-MM-dd HH:mm:ss}", + candidate.CategoryDisplay, + candidate.StatisticDisplay, + candidate.FirstDocumentPath, + DateTime.Now), + }; + } + + public string FormatIssue(RuleValidationIssue issue) + { + return string.Format( + "[{0}] 规则#{1} ({2}/{3}) {4}", + issue.Severity, + issue.Index + 1, + issue.RuleId ?? "-", + issue.RuleName ?? "-", + issue.Message); + } + + private static RuleValidationIssue CreateIssue(RuleValidationSeverity severity, int index, RuleDefinition rule, string message) + { + return new RuleValidationIssue + { + Severity = severity, + Index = index, + RuleId = rule.Id, + RuleName = rule.Name, + Message = message, + }; + } + + private static RegexOptions GetRegexOptions(RuleDefinition rule) + { + var options = RegexOptions.CultureInvariant; + if (!rule.IsCaseSensitive) + { + options |= RegexOptions.IgnoreCase; + } + + return options; + } + + private static void ValidateRegexRule(RuleValidationReport report, int index, RuleDefinition rule, Regex regex) + { + if (MatchesEmptyText(regex)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "正则表达式可匹配零长度文本,执行时可能导致死循环。")); + } + + try + { + regex.Replace("validation", rule.ReplacementText ?? string.Empty, 1); + } + catch (Exception ex) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "替换模板非法: " + ex.Message)); + } + + if (HasNestedQuantifierRisk(rule.SearchText)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "正则表达式存在嵌套量词风险,可能导致性能急剧下降。")); + } + + if (MayTimeoutDuringValidation(regex)) + { + report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "正则表达式验证探测时出现超时风险,建议简化表达式。")); + } + } + + private static bool MatchesEmptyText(Regex regex) + { + var emptyMatch = regex.Match(string.Empty); + if (emptyMatch.Success && emptyMatch.Length == 0) + { + return true; + } + + var sampleMatch = regex.Match("A"); + return sampleMatch.Success && sampleMatch.Length == 0; + } + + private static bool IsRegexReplacementSelfMatching(Regex regex, string replacementText) + { + try + { + return regex.Match(replacementText ?? string.Empty).Success; + } + catch + { + return false; + } + } + + private static bool MayTimeoutDuringValidation(Regex regex) + { + try + { + _ = regex.IsMatch(new string('a', 4096) + "!"); + return false; + } + catch (RegexMatchTimeoutException) + { + return true; + } + } + + private static bool HasNestedQuantifierRisk(string pattern) + { + if (string.IsNullOrWhiteSpace(pattern)) + { + return false; + } + + return Regex.IsMatch( + pattern, + @"\((?:[^()\\]|\\.)+[+*](?:[^()\\]|\\.)*\)[+*{]", + RegexOptions.CultureInvariant); + } + + private static void AddCompatibilityIssues(RuleValidationReport report, ValidatedRuleContext previous, ValidatedRuleContext current) + { + if (previous.Rule.Target != current.Rule.Target) + { + return; + } + + if (IsEquivalentSearchText(previous.Rule.SearchText, current.Rule.SearchText)) + { + report.Issues.Add(CreateIssue( + RuleValidationSeverity.Warning, + current.Index, + current.Rule, + string.Format("当前规则与 #{0} 使用相同原文本或表达式,请确认顺序影响。", previous.Index + 1))); + + if (HaveDifferentMatchingOptions(previous.Rule, current.Rule)) + { + report.Issues.Add(CreateIssue( + RuleValidationSeverity.Warning, + current.Index, + current.Rule, + string.Format("当前规则与 #{0} 使用相同原文本但匹配选项或替换文本不同,继续执行结果可能不稳定。", previous.Index + 1))); + } + } + + if (previous.Rule.MatchMode == RuleMatchMode.PlainText + && current.Rule.MatchMode == RuleMatchMode.PlainText + && HasPlainTextOverlap(previous.Rule, current.Rule)) + { + report.Issues.Add(CreateIssue( + RuleValidationSeverity.Warning, + current.Index, + current.Rule, + string.Format("当前规则与 #{0} 存在重叠命中风险,请确认执行顺序。", previous.Index + 1))); + } + } + + private static bool IsEquivalentSearchText(string left, string right) + { + return string.Equals(left ?? string.Empty, right ?? string.Empty, StringComparison.OrdinalIgnoreCase); + } + + private static bool HaveDifferentMatchingOptions(RuleDefinition left, RuleDefinition right) + { + return left.MatchMode != right.MatchMode + || left.IsCaseSensitive != right.IsCaseSensitive + || left.IsWholeWord != right.IsWholeWord + || !string.Equals(left.ReplacementText ?? string.Empty, right.ReplacementText ?? string.Empty, StringComparison.Ordinal); + } + + private static bool HasPlainTextOverlap(RuleDefinition left, RuleDefinition right) + { + if (string.IsNullOrEmpty(left.SearchText) || string.IsNullOrEmpty(right.SearchText)) + { + return false; + } + + if (left.IsWholeWord && right.IsWholeWord) + { + return false; + } + + var comparison = left.IsCaseSensitive && right.IsCaseSensitive + ? StringComparison.Ordinal + : StringComparison.OrdinalIgnoreCase; + + return left.SearchText.IndexOf(right.SearchText, comparison) >= 0 + || right.SearchText.IndexOf(left.SearchText, comparison) >= 0; + } + + private static string GetPrefix(AutoExtractCategory category) + { + switch (category) + { + case AutoExtractCategory.Keyword: + return "AK"; + case AutoExtractCategory.HighFrequencyWord: + return "AW"; + default: + return "AS"; + } + } + + private static string GetNamePrefix(AutoExtractCategory category) + { + switch (category) + { + case AutoExtractCategory.Keyword: + return "AUTO-KW"; + case AutoExtractCategory.HighFrequencyWord: + return "AUTO-WORD"; + default: + return "AUTO-SENT"; + } + } + + private sealed class ValidatedRuleContext + { + public ValidatedRuleContext(int index, RuleDefinition rule, Regex compiledRegex) + { + Index = index; + Rule = rule; + CompiledRegex = compiledRegex; + } + + public int Index { get; private set; } + + public RuleDefinition Rule { get; private set; } + + public Regex CompiledRegex { get; private set; } + } + } +} diff --git a/DCIT.Core/Services/RuntimeEnvironmentValidationService.cs b/DCIT.Core/Services/RuntimeEnvironmentValidationService.cs new file mode 100644 index 0000000..ad5da56 --- /dev/null +++ b/DCIT.Core/Services/RuntimeEnvironmentValidationService.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using DCIT.Core.Models; + +namespace DCIT.Core.Services +{ + public class RuntimeEnvironmentValidationService + { + private static readonly string[] WpsProgIds = + { + "KWPS.Application", + "kwps.Application", + "WPS.Application", + "wps.Application", + }; + + private readonly string _startupDirectory; + + public RuntimeEnvironmentValidationService(string startupDirectory = null) + { + _startupDirectory = string.IsNullOrWhiteSpace(startupDirectory) + ? AppContext.BaseDirectory + : startupDirectory; + } + + public RuntimeEnvironmentValidationResult Validate() + { + var result = new RuntimeEnvironmentValidationResult + { + StartupDirectory = _startupDirectory, + }; + + result.IsStartupDirectoryWritable = CanWriteToDirectory(_startupDirectory); + if (!result.IsStartupDirectoryWritable) + { + result.BlockingIssues.Add("程序启动目录不可写。请将程序部署到当前用户具有写权限的目录后再启动。"); + } + + result.ResolvedAutomationProgId = ResolveAutomationProgId(); + if (string.IsNullOrWhiteSpace(result.ResolvedAutomationProgId)) + { + result.BlockingIssues.Add("未检测到可自动化的 Microsoft Word 或 WPS 组件。当前环境无法满足目录/域刷新及 .doc 处理要求。"); + } + + result.ResolvedDocToPath = ResolveDocToPath(); + if (string.IsNullOrWhiteSpace(result.ResolvedDocToPath)) + { + result.Warnings.Add("未检测到 docto.exe,.doc 兼容性将仅依赖 Word/WPS 自动化链路。"); + } + + return result; + } + + protected virtual IEnumerable GetOfficeAutomationProgIds() + { + yield return "Word.Application"; + foreach (var progId in WpsProgIds) + { + yield return progId; + } + } + + protected virtual bool IsProgIdRegistered(string progId) + { + return !string.IsNullOrWhiteSpace(progId) + && Type.GetTypeFromProgID(progId, false) != null; + } + + protected virtual string ResolveDocToPath() + { + var candidates = new List(); + var envPath = Environment.GetEnvironmentVariable("DCIT_DOCTO_PATH"); + if (!string.IsNullOrWhiteSpace(envPath)) + { + candidates.Add(envPath); + } + + candidates.Add(Path.Combine(_startupDirectory, "docto.exe")); + candidates.Add(Path.Combine(_startupDirectory, "tools", "DocTo", "docto.exe")); + + foreach (var candidate in candidates) + { + if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate)) + { + return candidate; + } + } + + var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + foreach (var segment in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + try + { + var candidate = Path.Combine(segment.Trim(), "docto.exe"); + if (File.Exists(candidate)) + { + return candidate; + } + } + catch + { + } + } + + return null; + } + + protected virtual bool CanWriteToDirectory(string directoryPath) + { + if (string.IsNullOrWhiteSpace(directoryPath)) + { + return false; + } + + try + { + Directory.CreateDirectory(directoryPath); + var probePath = Path.Combine(directoryPath, ".dcit-writecheck-" + Guid.NewGuid().ToString("N") + ".tmp"); + File.WriteAllText(probePath, "probe"); + File.Delete(probePath); + return true; + } + catch + { + return false; + } + } + + private string ResolveAutomationProgId() + { + foreach (var progId in GetOfficeAutomationProgIds()) + { + if (IsProgIdRegistered(progId)) + { + return progId; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/DCIT.Core/Services/TemporaryFileCleanupService.cs b/DCIT.Core/Services/TemporaryFileCleanupService.cs new file mode 100644 index 0000000..4ec0f7b --- /dev/null +++ b/DCIT.Core/Services/TemporaryFileCleanupService.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using DCIT.Core.Models; + +namespace DCIT.Core.Services +{ + public sealed class TemporaryFileCleanupService + { + private static readonly Regex AtomicTempFilePattern = new Regex(@"\.tmp-[0-9a-fA-F]{32}(?=\.)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly string _tempRoot; + + public TemporaryFileCleanupService() + : this(Path.Combine(Path.GetTempPath(), "DCIT")) + { + } + + public TemporaryFileCleanupService(string tempRoot) + { + _tempRoot = string.IsNullOrWhiteSpace(tempRoot) + ? Path.Combine(Path.GetTempPath(), "DCIT") + : tempRoot; + } + + public TemporaryCleanupResult Cleanup(AppConfig config) + { + var result = new TemporaryCleanupResult(); + CleanupDedicatedTempDirectory(Path.Combine(_tempRoot, "processing"), result); + CleanupDedicatedTempDirectory(Path.Combine(_tempRoot, "extract"), result); + + var configuredOutputDirectories = new HashSet(StringComparer.OrdinalIgnoreCase); + AddDirectory(configuredOutputDirectories, config == null || config.ReplacePage == null ? null : config.ReplacePage.OutputDirectory); + AddDirectory(configuredOutputDirectories, config == null || config.RestorePage == null ? null : config.RestorePage.RestoreDirectory); + + foreach (var directory in configuredOutputDirectories) + { + CleanupAtomicTempFiles(directory, result); + } + + return result; + } + + private static void AddDirectory(ISet directories, string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + try + { + directories.Add(Path.GetFullPath(path)); + } + catch + { + } + } + + private static void CleanupAtomicTempFiles(string rootDirectory, TemporaryCleanupResult result) + { + if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) + { + return; + } + + IEnumerable files; + try + { + files = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories) + .Where(path => IsAtomicTempFile(Path.GetFileName(path))); + } + catch (Exception ex) + { + result.Failures.Add("无法扫描目录 " + rootDirectory + ": " + ex.Message); + return; + } + + foreach (var file in files) + { + TryDeleteFile(file, result); + } + } + + private static bool IsAtomicTempFile(string fileName) + { + return !string.IsNullOrWhiteSpace(fileName) && AtomicTempFilePattern.IsMatch(fileName); + } + + private static void CleanupDedicatedTempDirectory(string rootDirectory, TemporaryCleanupResult result) + { + if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) + { + return; + } + + IEnumerable files; + try + { + files = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories).ToList(); + } + catch (Exception ex) + { + result.Failures.Add("无法扫描目录 " + rootDirectory + ": " + ex.Message); + return; + } + + foreach (var file in files) + { + TryDeleteFile(file, result); + } + + TryDeleteEmptyDirectories(rootDirectory, result); + } + + private static void TryDeleteEmptyDirectories(string rootDirectory, TemporaryCleanupResult result) + { + IEnumerable directories; + try + { + directories = Directory.EnumerateDirectories(rootDirectory, "*", SearchOption.AllDirectories) + .OrderByDescending(path => path.Length) + .ToList(); + } + catch (Exception ex) + { + result.Failures.Add("无法枚举临时目录 " + rootDirectory + ": " + ex.Message); + return; + } + + foreach (var directory in directories) + { + try + { + if (!Directory.EnumerateFileSystemEntries(directory).Any()) + { + Directory.Delete(directory); + } + } + catch (Exception ex) + { + result.Failures.Add("无法删除临时目录 " + directory + ": " + ex.Message); + } + } + + try + { + if (!Directory.EnumerateFileSystemEntries(rootDirectory).Any()) + { + Directory.Delete(rootDirectory); + } + } + catch (Exception ex) + { + result.Failures.Add("无法删除临时目录 " + rootDirectory + ": " + ex.Message); + } + } + + private static void TryDeleteFile(string filePath, TemporaryCleanupResult result) + { + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + result.RemovedFiles.Add(filePath); + } + } + catch (Exception ex) + { + result.Failures.Add("无法删除临时文件 " + filePath + ": " + ex.Message); + } + } + } + + public sealed class TemporaryCleanupResult + { + public IList RemovedFiles { get; } = new List(); + + public IList Failures { get; } = new List(); + } +} diff --git a/DCIT.Core/Services/WordTextExtractionService.cs b/DCIT.Core/Services/WordTextExtractionService.cs new file mode 100644 index 0000000..e4f1e81 --- /dev/null +++ b/DCIT.Core/Services/WordTextExtractionService.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using DCIT.Core.Models; +using DCIT.Core.Utilities; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using W = DocumentFormat.OpenXml.Wordprocessing; + +namespace DCIT.Core.Services +{ + public class WordTextExtractionService + { + private readonly IDocConversionService _docConversionService; + + public WordTextExtractionService() + : this(new OfficeDocConversionService()) + { + } + + public WordTextExtractionService(IDocConversionService docConversionService) + { + _docConversionService = docConversionService; + } + + public ExtractedDocumentText ExtractVisibleText(string filePath) + { + var workingPath = filePath; + string temporaryDocxPath = null; + + try + { + var extension = Path.GetExtension(filePath) ?? string.Empty; + if (extension.Equals(".doc", StringComparison.OrdinalIgnoreCase)) + { + if (_docConversionService == null) + { + throw new NotSupportedException("当前环境未配置 .doc 转换服务。"); + } + + temporaryDocxPath = Path.Combine( + Path.GetTempPath(), + "DCIT", + "extract", + Guid.NewGuid().ToString("N") + ".docx"); + Directory.CreateDirectory(Path.GetDirectoryName(temporaryDocxPath) ?? Path.GetTempPath()); + _docConversionService.Convert(filePath, temporaryDocxPath); + workingPath = temporaryDocxPath; + } + + using (var document = WordprocessingDocument.Open(workingPath, false)) + { + var builder = new StringBuilder(); + var rootElements = GetRelevantRootElements(document); + foreach (var root in rootElements) + { + AppendVisibleText(root, builder); + if (builder.Length > 0 && builder[builder.Length - 1] != '\n') + { + builder.AppendLine(); + } + } + + return new ExtractedDocumentText + { + FilePath = filePath, + VisibleText = builder.ToString(), + }; + } + } + finally + { + TryDelete(temporaryDocxPath); + } + } + + private static IEnumerable GetRelevantRootElements(WordprocessingDocument document) + { + var result = new List>(); + var partQueue = new Queue(); + partQueue.Enqueue(document.MainDocumentPart); + var visited = new HashSet(); + + while (partQueue.Count > 0) + { + var container = partQueue.Dequeue(); + foreach (var pair in container.Parts) + { + var part = pair.OpenXmlPart; + if (part == null || !visited.Add(part)) + { + continue; + } + + if (part.RootElement != null && IsRelevantPart(part)) + { + result.Add(new KeyValuePair(GetStablePartKey(part), part.RootElement)); + } + + partQueue.Enqueue(part); + } + } + + var ordered = result + .OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase) + .Select(item => item.Value) + .ToList(); + + if (document.MainDocumentPart != null && document.MainDocumentPart.Document != null) + { + ordered.Insert(0, document.MainDocumentPart.Document); + } + + return ordered; + } + + private static bool IsRelevantPart(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 GetStablePartKey(OpenXmlPart part) + { + return part == null || part.Uri == null + ? string.Empty + : part.Uri.ToString(); + } + + private static void AppendVisibleText(OpenXmlElement root, StringBuilder builder) + { + foreach (var element in root.Descendants()) + { + var leaf = element as OpenXmlLeafTextElement; + if (leaf != null) + { + if (!IsVisibleLeaf(leaf)) + { + continue; + } + + var text = leaf.Text; + if (string.IsNullOrWhiteSpace(text)) + { + continue; + } + + builder.Append(text); + builder.Append(' '); + continue; + } + + if (!VmlTextMetadataUtility.IsVmlShapeElement(element)) + { + continue; + } + + foreach (var binding in VmlTextMetadataUtility.CreateBindings(element)) + { + if (string.IsNullOrWhiteSpace(binding.Text)) + { + continue; + } + + builder.Append(binding.Text); + builder.Append(' '); + } + } + } + + private static bool IsVisibleLeaf(OpenXmlLeafTextElement leaf) + { + if (leaf is W.FieldCode || leaf is W.DeletedText) + { + return false; + } + + if (leaf.Ancestors().Any()) + { + return false; + } + + var run = leaf.Ancestors().FirstOrDefault(); + if (run != null && run.RunProperties != null && run.RunProperties.Vanish != null) + { + return false; + } + + return true; + } + + private static void TryDelete(string path) + { + try + { + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + } + } + } +} diff --git a/DCIT.Core/Utilities/DisplayWidthUtility.cs b/DCIT.Core/Utilities/DisplayWidthUtility.cs new file mode 100644 index 0000000..c605159 --- /dev/null +++ b/DCIT.Core/Utilities/DisplayWidthUtility.cs @@ -0,0 +1,103 @@ +using System; +using System.Text; + +namespace DCIT.Core.Utilities +{ + public static class DisplayWidthUtility + { + public static int Measure(string value) + { + if (string.IsNullOrEmpty(value)) + { + return 0; + } + + var width = 0; + foreach (var character in value) + { + width += GetCharacterWidth(character); + } + + return width; + } + + public static string TruncateToWidth(string value, int maxWidth, out bool truncated) + { + if (string.IsNullOrEmpty(value) || maxWidth <= 0) + { + truncated = !string.IsNullOrEmpty(value); + return string.Empty; + } + + var builder = new StringBuilder(); + var width = 0; + truncated = false; + + foreach (var character in value) + { + var charWidth = GetCharacterWidth(character); + if (width + charWidth > maxWidth) + { + truncated = true; + break; + } + + builder.Append(character); + width += charWidth; + } + + if (!truncated && builder.Length < value.Length) + { + truncated = true; + } + + return builder.ToString(); + } + + public static string PadToWidth(string value, int targetWidth, out bool padded) + { + var builder = new StringBuilder(value ?? string.Empty); + var currentWidth = Measure(builder.ToString()); + padded = currentWidth < targetWidth; + + while (currentWidth + 2 <= targetWidth) + { + builder.Append('\u3000'); + currentWidth += 2; + } + + while (currentWidth < targetWidth) + { + builder.Append(' '); + currentWidth += 1; + } + + return builder.ToString(); + } + + private static int GetCharacterWidth(char character) + { + if (character == '\u3000') + { + return 2; + } + + if (character >= 0x1100 && + (character <= 0x115F || + character == 0x2329 || + character == 0x232A || + (character >= 0x2E80 && character <= 0xA4CF && character != 0x303F) || + (character >= 0xAC00 && character <= 0xD7A3) || + (character >= 0xF900 && character <= 0xFAFF) || + (character >= 0xFE10 && character <= 0xFE19) || + (character >= 0xFE30 && character <= 0xFE6F) || + (character >= 0xFF00 && character <= 0xFF60) || + (character >= 0xFFE0 && character <= 0xFFE6))) + { + return 2; + } + + return 1; + } + } +} diff --git a/DCIT.Core/Utilities/HashUtility.cs b/DCIT.Core/Utilities/HashUtility.cs new file mode 100644 index 0000000..3ccf300 --- /dev/null +++ b/DCIT.Core/Utilities/HashUtility.cs @@ -0,0 +1,270 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace DCIT.Core.Utilities +{ + public static class HashUtility + { + private static readonly byte[] EncryptionKey = ComputeSha256Bytes("DCIT::DiffEncryptionKey::20260424"); + private static readonly byte[] TamperKey = ComputeSha256Bytes("DCIT::TamperProtectionKey::20260424"); + + public static string ComputeSha256Base64(byte[] bytes) + { + using (var sha = SHA256.Create()) + { + return Convert.ToBase64String(sha.ComputeHash(bytes ?? new byte[0])); + } + } + + public static string ComputeSha256Base64(string value) + { + return ComputeSha256Base64(Encoding.UTF8.GetBytes(value ?? string.Empty)); + } + + public static string ComputeFileSha256Base64(string path) + { + using (var stream = File.OpenRead(path)) + using (var sha = SHA256.Create()) + { + return Convert.ToBase64String(sha.ComputeHash(stream)); + } + } + + public static string ComputeHmacBase64(byte[] bytes) + { + using (var hmac = new HMACSHA256(TamperKey)) + { + return Convert.ToBase64String(hmac.ComputeHash(bytes ?? new byte[0])); + } + } + + public static string ComputeFileHmacBase64(string path) + { + using (var stream = File.OpenRead(path)) + using (var hmac = new HMACSHA256(TamperKey)) + { + return Convert.ToBase64String(hmac.ComputeHash(stream)); + } + } + + public static EncryptedPayload Encrypt(byte[] plaintextBytes) + { + var iv = new byte[16]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(iv); + } + + byte[] cipherBytes; + using (var aes = Aes.Create()) + { + aes.Key = EncryptionKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + using (var encryptor = aes.CreateEncryptor()) + { + cipherBytes = encryptor.TransformFinalBlock(plaintextBytes ?? new byte[0], 0, (plaintextBytes ?? new byte[0]).Length); + } + } + + var tagInput = Combine(iv, cipherBytes); + using (var hmac = new HMACSHA256(TamperKey)) + { + return new EncryptedPayload + { + CiphertextBase64 = Convert.ToBase64String(cipherBytes), + NonceBase64 = Convert.ToBase64String(iv), + TagBase64 = Convert.ToBase64String(hmac.ComputeHash(tagInput)), + }; + } + } + + public static EncryptedPayloadHeader EncryptStream(Stream plaintextStream, Stream ciphertextStream) + { + if (plaintextStream == null) + { + throw new ArgumentNullException("plaintextStream"); + } + + if (ciphertextStream == null) + { + throw new ArgumentNullException("ciphertextStream"); + } + + if (!ciphertextStream.CanSeek) + { + throw new ArgumentException("密文流必须支持 Seek 以便计算防篡改标签。", "ciphertextStream"); + } + + var iv = new byte[16]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(iv); + } + + using (var aes = Aes.Create()) + { + aes.Key = EncryptionKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + using (var encryptor = aes.CreateEncryptor()) + using (var cryptoStream = new CryptoStream(ciphertextStream, encryptor, CryptoStreamMode.Write, leaveOpen: true)) + { + plaintextStream.CopyTo(cryptoStream); + cryptoStream.FlushFinalBlock(); + } + } + + ciphertextStream.Position = 0; + using (var hmac = new HMACSHA256(TamperKey)) + { + hmac.TransformBlock(iv, 0, iv.Length, iv, 0); + var buffer = new byte[65536]; + int read; + while ((read = ciphertextStream.Read(buffer, 0, buffer.Length)) > 0) + { + hmac.TransformBlock(buffer, 0, read, buffer, 0); + } + + hmac.TransformFinalBlock(new byte[0], 0, 0); + return new EncryptedPayloadHeader + { + NonceBase64 = Convert.ToBase64String(iv), + TagBase64 = Convert.ToBase64String(hmac.Hash), + }; + } + } + + public static byte[] Decrypt(string ciphertextBase64, string nonceBase64, string tagBase64) + { + var cipherBytes = Convert.FromBase64String(ciphertextBase64 ?? string.Empty); + var iv = Convert.FromBase64String(nonceBase64 ?? string.Empty); + var providedTag = Convert.FromBase64String(tagBase64 ?? string.Empty); + var expectedTag = Convert.FromBase64String(ComputeHmacBase64(Combine(iv, cipherBytes))); + if (!FixedTimeEquals(providedTag, expectedTag)) + { + throw new CryptographicException("差异载荷验证失败。"); + } + + using (var aes = Aes.Create()) + { + aes.Key = EncryptionKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + using (var decryptor = aes.CreateDecryptor()) + { + return decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); + } + } + } + + public static void DecryptStream(Stream cipherStream, Stream plaintextStream, byte[] iv, byte[] expectedTag) + { + if (cipherStream == null) + { + throw new ArgumentNullException("cipherStream"); + } + + if (plaintextStream == null) + { + throw new ArgumentNullException("plaintextStream"); + } + + if (!cipherStream.CanSeek) + { + throw new ArgumentException("密文流必须支持 Seek 以便校验防篡改标签。", "cipherStream"); + } + + using (var hmac = new HMACSHA256(TamperKey)) + { + hmac.TransformBlock(iv ?? new byte[0], 0, (iv ?? new byte[0]).Length, iv ?? new byte[0], 0); + cipherStream.Position = 0; + var buffer = new byte[65536]; + int read; + while ((read = cipherStream.Read(buffer, 0, buffer.Length)) > 0) + { + hmac.TransformBlock(buffer, 0, read, buffer, 0); + } + + hmac.TransformFinalBlock(new byte[0], 0, 0); + if (!FixedTimeEquals(hmac.Hash, expectedTag ?? new byte[0])) + { + throw new CryptographicException("差异载荷防篡改校验失败。"); + } + } + + cipherStream.Position = 0; + using (var aes = Aes.Create()) + { + aes.Key = EncryptionKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + using (var decryptor = aes.CreateDecryptor()) + using (var cryptoStream = new CryptoStream(cipherStream, decryptor, CryptoStreamMode.Read, leaveOpen: true)) + { + cryptoStream.CopyTo(plaintextStream); + } + } + } + + private static byte[] ComputeSha256Bytes(string value) + { + using (var sha = SHA256.Create()) + { + return sha.ComputeHash(Encoding.UTF8.GetBytes(value ?? string.Empty)); + } + } + + private static bool FixedTimeEquals(byte[] left, byte[] right) + { + if (left == null || right == null || left.Length != right.Length) + { + return false; + } + + var diff = 0; + for (var index = 0; index < left.Length; index++) + { + diff |= left[index] ^ right[index]; + } + + return diff == 0; + } + + private static byte[] Combine(byte[] left, byte[] right) + { + left = left ?? new byte[0]; + right = right ?? new byte[0]; + var bytes = new byte[left.Length + right.Length]; + Buffer.BlockCopy(left, 0, bytes, 0, left.Length); + Buffer.BlockCopy(right, 0, bytes, left.Length, right.Length); + return bytes; + } + } + + public class EncryptedPayload + { + public string CiphertextBase64 { get; set; } + + public string NonceBase64 { get; set; } + + public string TagBase64 { get; set; } + } + + public class EncryptedPayloadHeader + { + public string NonceBase64 { get; set; } + + public string TagBase64 { get; set; } + } +} diff --git a/DCIT.Core/Utilities/PathUtility.cs b/DCIT.Core/Utilities/PathUtility.cs new file mode 100644 index 0000000..0e46818 --- /dev/null +++ b/DCIT.Core/Utilities/PathUtility.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace DCIT.Core.Utilities +{ + public static class PathUtility + { + public static string NormalizeDirectory(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return string.Empty; + } + + var fullPath = Path.GetFullPath(path.Trim()); + if (!fullPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) + { + fullPath += Path.DirectorySeparatorChar; + } + + return fullPath; + } + + public static bool AreSameOrNestedDirectories(string left, string right) + { + var normalizedLeft = NormalizeDirectory(left); + var normalizedRight = NormalizeDirectory(right); + if (string.IsNullOrEmpty(normalizedLeft) || string.IsNullOrEmpty(normalizedRight)) + { + return false; + } + + return normalizedLeft.Equals(normalizedRight, StringComparison.OrdinalIgnoreCase) + || normalizedLeft.StartsWith(normalizedRight, StringComparison.OrdinalIgnoreCase) + || normalizedRight.StartsWith(normalizedLeft, StringComparison.OrdinalIgnoreCase); + } + + public static string GetRelativePath(string relativeTo, string path) + { + var root = new Uri(NormalizeDirectory(relativeTo), UriKind.Absolute); + var target = new Uri(Path.GetFullPath(path), UriKind.Absolute); + var relative = root.MakeRelativeUri(target).ToString().Replace('/', Path.DirectorySeparatorChar); + return Uri.UnescapeDataString(relative); + } + + public static IEnumerable ValidateDirectoryPair(string firstLabel, string firstPath, string secondLabel, string secondPath) + { + if (string.IsNullOrWhiteSpace(firstPath) || string.IsNullOrWhiteSpace(secondPath)) + { + yield break; + } + + if (AreSameOrNestedDirectories(firstPath, secondPath)) + { + yield return string.Format("{0} 与 {1} 不能相同,也不能互为父子目录。", firstLabel, secondLabel); + } + } + } +} diff --git a/DCIT.Core/Utilities/VmlTextMetadataUtility.cs b/DCIT.Core/Utilities/VmlTextMetadataUtility.cs new file mode 100644 index 0000000..e2be8c3 --- /dev/null +++ b/DCIT.Core/Utilities/VmlTextMetadataUtility.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using DocumentFormat.OpenXml; + +namespace DCIT.Core.Utilities +{ + internal static class VmlTextMetadataUtility + { + private static readonly Regex Base64WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled); + + public static bool IsVmlShapeElement(OpenXmlElement element) + { + return element != null + && string.Equals(element.LocalName, "shape", StringComparison.OrdinalIgnoreCase) + && string.Equals(element.NamespaceUri, "urn:schemas-microsoft-com:vml", StringComparison.OrdinalIgnoreCase); + } + + public static IEnumerable CreateBindings(OpenXmlElement shape) + { + if (!IsVmlShapeElement(shape)) + { + yield break; + } + + foreach (var binding in CreateEquationBindings(shape)) + { + yield return binding; + } + + foreach (var binding in CreateChartBindings(shape)) + { + yield return binding; + } + } + + private static IEnumerable CreateEquationBindings(OpenXmlElement shape) + { + var attribute = FindAttribute(shape, "equationxml"); + if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value)) + { + yield break; + } + + var actualAttribute = attribute.Value; + + XDocument document; + try + { + var decoded = WebUtility.HtmlDecode(actualAttribute.Value); + if (string.IsNullOrWhiteSpace(decoded)) + { + yield break; + } + + document = XDocument.Parse(decoded, LoadOptions.PreserveWhitespace); + } + catch + { + yield break; + } + + var textNodes = document + .Descendants() + .Where(item => string.Equals(item.Name.LocalName, "t", StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (textNodes.Count == 0) + { + yield break; + } + + foreach (var textNode in textNodes) + { + var initialText = textNode.Value ?? string.Empty; + if (initialText.Length == 0) + { + continue; + } + + yield return new VmlTextBinding + { + Text = initialText, + IsEquationText = true, + Apply = updatedText => + { + textNode.Value = updatedText ?? string.Empty; + SetAttribute(shape, actualAttribute, SerializeXml(document)); + }, + }; + } + } + + private static IEnumerable CreateChartBindings(OpenXmlElement shape) + { + var attribute = FindAttribute(shape, "gfxdata"); + if (!attribute.HasValue || string.IsNullOrWhiteSpace(attribute.Value.Value)) + { + yield break; + } + + var actualAttribute = attribute.Value; + + ChartPackageContext context; + try + { + context = ChartPackageContext.TryCreate(shape, actualAttribute); + } + catch + { + context = null; + } + + if (context == null || context.XmlEntries.Count == 0) + { + yield break; + } + + foreach (var entry in context.XmlEntries) + { + foreach (var textNode in entry.Document + .Descendants() + .Where(item => IsChartVisibleTextNode(item)) + .ToList()) + { + var initialText = textNode.Value ?? string.Empty; + if (initialText.Length == 0) + { + continue; + } + + yield return new VmlTextBinding + { + Text = initialText, + IsChartText = true, + Apply = updatedText => + { + textNode.Value = updatedText ?? string.Empty; + context.Save(); + }, + }; + } + } + } + + private static bool IsChartVisibleTextNode(XElement element) + { + if (element == null) + { + return false; + } + + var localName = element.Name.LocalName; + return string.Equals(localName, "t", StringComparison.OrdinalIgnoreCase) + || string.Equals(localName, "v", StringComparison.OrdinalIgnoreCase); + } + + private static OpenXmlAttribute? FindAttribute(OpenXmlElement element, string localName) + { + if (element == null || string.IsNullOrWhiteSpace(localName)) + { + return null; + } + + foreach (var attribute in element.GetAttributes()) + { + if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase)) + { + return attribute; + } + } + + return null; + } + + private static void SetAttribute(OpenXmlElement element, OpenXmlAttribute sourceAttribute, string value) + { + element.SetAttribute(new OpenXmlAttribute( + sourceAttribute.Prefix, + sourceAttribute.LocalName, + sourceAttribute.NamespaceUri, + value ?? string.Empty)); + } + + private static string SerializeXml(XDocument document) + { + using (var writer = new Utf8StringWriter()) + using (var xmlWriter = XmlWriter.Create(writer, new XmlWriterSettings + { + OmitXmlDeclaration = document.Declaration == null, + Encoding = new UTF8Encoding(false), + Indent = false, + NewLineHandling = NewLineHandling.None, + })) + { + document.Save(xmlWriter); + xmlWriter.Flush(); + return writer.ToString(); + } + } + + private sealed class ChartPackageContext + { + private readonly OpenXmlElement _shape; + private readonly OpenXmlAttribute _attribute; + private readonly List _allEntries; + + private ChartPackageContext(OpenXmlElement shape, OpenXmlAttribute attribute, List allEntries, List xmlEntries) + { + _shape = shape; + _attribute = attribute; + _allEntries = allEntries; + XmlEntries = xmlEntries; + } + + public List XmlEntries { get; private set; } + + public static ChartPackageContext TryCreate(OpenXmlElement shape, OpenXmlAttribute attribute) + { + var decoded = WebUtility.HtmlDecode(attribute.Value ?? string.Empty); + if (string.IsNullOrWhiteSpace(decoded)) + { + return null; + } + + var normalizedBase64 = Base64WhitespaceRegex.Replace(decoded, string.Empty); + if (normalizedBase64.Length == 0) + { + return null; + } + + var bytes = Convert.FromBase64String(normalizedBase64); + var allEntries = new List(); + var xmlEntries = new List(); + + using (var input = new MemoryStream(bytes)) + using (var zip = new ZipArchive(input, ZipArchiveMode.Read, false)) + { + foreach (var entry in zip.Entries) + { + var state = new EntryState + { + FullName = entry.FullName, + LastWriteTime = entry.LastWriteTime, + }; + + using (var entryStream = entry.Open()) + using (var memory = new MemoryStream()) + { + entryStream.CopyTo(memory); + state.RawBytes = memory.ToArray(); + } + + if (entry.FullName.IndexOf("drs/charts/", StringComparison.OrdinalIgnoreCase) >= 0 + && entry.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + { + try + { + state.Document = XDocument.Parse( + Encoding.UTF8.GetString(state.RawBytes), + LoadOptions.PreserveWhitespace); + } + catch + { + state.Document = null; + } + + if (state.Document != null) + { + xmlEntries.Add(state); + } + } + + allEntries.Add(state); + } + } + + return new ChartPackageContext(shape, attribute, allEntries, xmlEntries); + } + + public void Save() + { + using (var output = new MemoryStream()) + { + using (var zip = new ZipArchive(output, ZipArchiveMode.Create, true)) + { + foreach (var entry in _allEntries) + { + var targetEntry = zip.CreateEntry(entry.FullName, CompressionLevel.Optimal); + targetEntry.LastWriteTime = entry.LastWriteTime; + using (var targetStream = targetEntry.Open()) + { + var bytes = entry.Document == null + ? (entry.RawBytes ?? Array.Empty()) + : Encoding.UTF8.GetBytes(SerializeXml(entry.Document)); + targetStream.Write(bytes, 0, bytes.Length); + } + } + } + + SetAttribute(_shape, _attribute, Convert.ToBase64String(output.ToArray())); + } + } + } + + private sealed class Utf8StringWriter : StringWriter + { + public override Encoding Encoding + { + get { return new UTF8Encoding(false); } + } + } + + private sealed class EntryState + { + public string FullName { get; set; } + + public DateTimeOffset LastWriteTime { get; set; } + + public byte[] RawBytes { get; set; } + + public XDocument Document { get; set; } + } + } + + internal sealed class VmlTextBinding + { + public string Text { get; set; } + + public bool IsEquationText { get; set; } + + public bool IsChartText { get; set; } + + public Action Apply { get; set; } + } +} diff --git a/DCIT.Core/bin/Debug/net48/DCIT.Core.dll b/DCIT.Core/bin/Debug/net48/DCIT.Core.dll new file mode 100644 index 0000000..fb14d3f Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/DCIT.Core.dll differ diff --git a/DCIT.Core/bin/Debug/net48/DCIT.Core.pdb b/DCIT.Core/bin/Debug/net48/DCIT.Core.pdb new file mode 100644 index 0000000..c8870ce Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/DCIT.Core.pdb differ diff --git a/DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.Framework.dll b/DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.Framework.dll new file mode 100644 index 0000000..1407d6b Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.Framework.dll differ diff --git a/DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.dll b/DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.dll new file mode 100644 index 0000000..92b3b68 Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/DocumentFormat.OpenXml.dll differ diff --git a/DCIT.Core/bin/Debug/net48/ExCSS.dll b/DCIT.Core/bin/Debug/net48/ExCSS.dll new file mode 100644 index 0000000..aca855d Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/ExCSS.dll differ diff --git a/DCIT.Core/bin/Debug/net48/Newtonsoft.Json.dll b/DCIT.Core/bin/Debug/net48/Newtonsoft.Json.dll new file mode 100644 index 0000000..3f6541a Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/Newtonsoft.Json.dll differ diff --git a/DCIT.Core/bin/Debug/net48/Svg.dll b/DCIT.Core/bin/Debug/net48/Svg.dll new file mode 100644 index 0000000..b50b076 Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/Svg.dll differ diff --git a/DCIT.Core/bin/Debug/net48/System.Buffers.dll b/DCIT.Core/bin/Debug/net48/System.Buffers.dll new file mode 100644 index 0000000..f2d83c5 Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/System.Buffers.dll differ diff --git a/DCIT.Core/bin/Debug/net48/System.Memory.dll b/DCIT.Core/bin/Debug/net48/System.Memory.dll new file mode 100644 index 0000000..4617199 Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/System.Memory.dll differ diff --git a/DCIT.Core/bin/Debug/net48/System.Numerics.Vectors.dll b/DCIT.Core/bin/Debug/net48/System.Numerics.Vectors.dll new file mode 100644 index 0000000..0865972 Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/System.Numerics.Vectors.dll differ diff --git a/DCIT.Core/bin/Debug/net48/System.Runtime.CompilerServices.Unsafe.dll b/DCIT.Core/bin/Debug/net48/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000..de9e124 Binary files /dev/null and b/DCIT.Core/bin/Debug/net48/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.deps.json b/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.deps.json new file mode 100644 index 0000000..76320bf --- /dev/null +++ b/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.deps.json @@ -0,0 +1,194 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "DCIT.Core/1.0.0": { + "dependencies": { + "DocumentFormat.OpenXml": "3.5.1", + "Newtonsoft.Json": "13.0.4", + "Svg": "3.4.7" + }, + "runtime": { + "DCIT.Core.dll": {} + } + }, + "DocumentFormat.OpenXml/3.5.1": { + "dependencies": { + "DocumentFormat.OpenXml.Framework": "3.5.1" + }, + "runtime": { + "lib/netstandard2.0/DocumentFormat.OpenXml.dll": { + "assemblyVersion": "3.5.1.0", + "fileVersion": "3.5.1.0" + } + } + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "dependencies": { + "System.IO.Packaging": "8.0.1" + }, + "runtime": { + "lib/net6.0/DocumentFormat.OpenXml.Framework.dll": { + "assemblyVersion": "3.5.1.0", + "fileVersion": "3.5.1.0" + } + } + }, + "ExCSS/4.2.3": { + "runtime": { + "lib/net6.0/ExCSS.dll": { + "assemblyVersion": "4.2.3.0", + "fileVersion": "4.2.3.0" + } + } + }, + "Microsoft.NETCore.Platforms/5.0.0": {}, + "Microsoft.Win32.SystemEvents/5.0.0": { + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": { + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + }, + "runtimeTargets": { + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + } + }, + "Newtonsoft.Json/13.0.4": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.4.30916" + } + } + }, + "Svg/3.4.7": { + "dependencies": { + "ExCSS": "4.2.3", + "System.Drawing.Common": "5.0.3" + }, + "runtime": { + "lib/net6.0/Svg.dll": { + "assemblyVersion": "3.4.0.0", + "fileVersion": "3.4.7.1" + } + } + }, + "System.Drawing.Common/5.0.3": { + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + }, + "runtime": { + "lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + } + }, + "runtimeTargets": { + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "rid": "unix", + "assetType": "runtime", + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + }, + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + } + } + }, + "System.IO.Packaging/8.0.1": { + "runtime": { + "lib/net6.0/System.IO.Packaging.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.1024.46610" + } + } + } + } + }, + "libraries": { + "DCIT.Core/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "DocumentFormat.OpenXml/3.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==", + "path": "documentformat.openxml/3.5.1", + "hashPath": "documentformat.openxml.3.5.1.nupkg.sha512" + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==", + "path": "documentformat.openxml.framework/3.5.1", + "hashPath": "documentformat.openxml.framework.3.5.1.nupkg.sha512" + }, + "ExCSS/4.2.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==", + "path": "excss/4.2.3", + "hashPath": "excss.4.2.3.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "path": "microsoft.netcore.platforms/5.0.0", + "hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512" + }, + "Microsoft.Win32.SystemEvents/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "path": "microsoft.win32.systemevents/5.0.0", + "hashPath": "microsoft.win32.systemevents.5.0.0.nupkg.sha512" + }, + "Newtonsoft.Json/13.0.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==", + "path": "newtonsoft.json/13.0.4", + "hashPath": "newtonsoft.json.13.0.4.nupkg.sha512" + }, + "Svg/3.4.7": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==", + "path": "svg/3.4.7", + "hashPath": "svg.3.4.7.nupkg.sha512" + }, + "System.Drawing.Common/5.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==", + "path": "system.drawing.common/5.0.3", + "hashPath": "system.drawing.common.5.0.3.nupkg.sha512" + }, + "System.IO.Packaging/8.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==", + "path": "system.io.packaging/8.0.1", + "hashPath": "system.io.packaging.8.0.1.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.dll b/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.dll new file mode 100644 index 0000000..ce6fde6 Binary files /dev/null and b/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.dll differ diff --git a/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.pdb b/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.pdb new file mode 100644 index 0000000..21c4915 Binary files /dev/null and b/DCIT.Core/bin/Debug/net6.0-windows/DCIT.Core.pdb differ diff --git a/DCIT.Core/bin/Release/net48/DCIT.Core.dll b/DCIT.Core/bin/Release/net48/DCIT.Core.dll new file mode 100644 index 0000000..ae58421 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/DCIT.Core.dll differ diff --git a/DCIT.Core/bin/Release/net48/DCIT.Core.pdb b/DCIT.Core/bin/Release/net48/DCIT.Core.pdb new file mode 100644 index 0000000..84baef7 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/DCIT.Core.pdb differ diff --git a/DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.Framework.dll b/DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.Framework.dll new file mode 100644 index 0000000..1407d6b Binary files /dev/null and b/DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.Framework.dll differ diff --git a/DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.dll b/DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.dll new file mode 100644 index 0000000..92b3b68 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/DocumentFormat.OpenXml.dll differ diff --git a/DCIT.Core/bin/Release/net48/ExCSS.dll b/DCIT.Core/bin/Release/net48/ExCSS.dll new file mode 100644 index 0000000..aca855d Binary files /dev/null and b/DCIT.Core/bin/Release/net48/ExCSS.dll differ diff --git a/DCIT.Core/bin/Release/net48/Newtonsoft.Json.dll b/DCIT.Core/bin/Release/net48/Newtonsoft.Json.dll new file mode 100644 index 0000000..3f6541a Binary files /dev/null and b/DCIT.Core/bin/Release/net48/Newtonsoft.Json.dll differ diff --git a/DCIT.Core/bin/Release/net48/Svg.dll b/DCIT.Core/bin/Release/net48/Svg.dll new file mode 100644 index 0000000..b50b076 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/Svg.dll differ diff --git a/DCIT.Core/bin/Release/net48/System.Buffers.dll b/DCIT.Core/bin/Release/net48/System.Buffers.dll new file mode 100644 index 0000000..f2d83c5 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/System.Buffers.dll differ diff --git a/DCIT.Core/bin/Release/net48/System.Memory.dll b/DCIT.Core/bin/Release/net48/System.Memory.dll new file mode 100644 index 0000000..4617199 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/System.Memory.dll differ diff --git a/DCIT.Core/bin/Release/net48/System.Numerics.Vectors.dll b/DCIT.Core/bin/Release/net48/System.Numerics.Vectors.dll new file mode 100644 index 0000000..0865972 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/System.Numerics.Vectors.dll differ diff --git a/DCIT.Core/bin/Release/net48/System.Runtime.CompilerServices.Unsafe.dll b/DCIT.Core/bin/Release/net48/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000..de9e124 Binary files /dev/null and b/DCIT.Core/bin/Release/net48/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.deps.json b/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.deps.json new file mode 100644 index 0000000..76320bf --- /dev/null +++ b/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.deps.json @@ -0,0 +1,194 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "DCIT.Core/1.0.0": { + "dependencies": { + "DocumentFormat.OpenXml": "3.5.1", + "Newtonsoft.Json": "13.0.4", + "Svg": "3.4.7" + }, + "runtime": { + "DCIT.Core.dll": {} + } + }, + "DocumentFormat.OpenXml/3.5.1": { + "dependencies": { + "DocumentFormat.OpenXml.Framework": "3.5.1" + }, + "runtime": { + "lib/netstandard2.0/DocumentFormat.OpenXml.dll": { + "assemblyVersion": "3.5.1.0", + "fileVersion": "3.5.1.0" + } + } + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "dependencies": { + "System.IO.Packaging": "8.0.1" + }, + "runtime": { + "lib/net6.0/DocumentFormat.OpenXml.Framework.dll": { + "assemblyVersion": "3.5.1.0", + "fileVersion": "3.5.1.0" + } + } + }, + "ExCSS/4.2.3": { + "runtime": { + "lib/net6.0/ExCSS.dll": { + "assemblyVersion": "4.2.3.0", + "fileVersion": "4.2.3.0" + } + } + }, + "Microsoft.NETCore.Platforms/5.0.0": {}, + "Microsoft.Win32.SystemEvents/5.0.0": { + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": { + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + }, + "runtimeTargets": { + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + } + }, + "Newtonsoft.Json/13.0.4": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.4.30916" + } + } + }, + "Svg/3.4.7": { + "dependencies": { + "ExCSS": "4.2.3", + "System.Drawing.Common": "5.0.3" + }, + "runtime": { + "lib/net6.0/Svg.dll": { + "assemblyVersion": "3.4.0.0", + "fileVersion": "3.4.7.1" + } + } + }, + "System.Drawing.Common/5.0.3": { + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + }, + "runtime": { + "lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + } + }, + "runtimeTargets": { + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "rid": "unix", + "assetType": "runtime", + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + }, + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + } + } + }, + "System.IO.Packaging/8.0.1": { + "runtime": { + "lib/net6.0/System.IO.Packaging.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.1024.46610" + } + } + } + } + }, + "libraries": { + "DCIT.Core/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "DocumentFormat.OpenXml/3.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==", + "path": "documentformat.openxml/3.5.1", + "hashPath": "documentformat.openxml.3.5.1.nupkg.sha512" + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==", + "path": "documentformat.openxml.framework/3.5.1", + "hashPath": "documentformat.openxml.framework.3.5.1.nupkg.sha512" + }, + "ExCSS/4.2.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==", + "path": "excss/4.2.3", + "hashPath": "excss.4.2.3.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "path": "microsoft.netcore.platforms/5.0.0", + "hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512" + }, + "Microsoft.Win32.SystemEvents/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "path": "microsoft.win32.systemevents/5.0.0", + "hashPath": "microsoft.win32.systemevents.5.0.0.nupkg.sha512" + }, + "Newtonsoft.Json/13.0.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==", + "path": "newtonsoft.json/13.0.4", + "hashPath": "newtonsoft.json.13.0.4.nupkg.sha512" + }, + "Svg/3.4.7": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==", + "path": "svg/3.4.7", + "hashPath": "svg.3.4.7.nupkg.sha512" + }, + "System.Drawing.Common/5.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==", + "path": "system.drawing.common/5.0.3", + "hashPath": "system.drawing.common.5.0.3.nupkg.sha512" + }, + "System.IO.Packaging/8.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==", + "path": "system.io.packaging/8.0.1", + "hashPath": "system.io.packaging.8.0.1.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.dll b/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.dll new file mode 100644 index 0000000..6c659e5 Binary files /dev/null and b/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.dll differ diff --git a/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.pdb b/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.pdb new file mode 100644 index 0000000..7c317a5 Binary files /dev/null and b/DCIT.Core/bin/Release/net6.0-windows/DCIT.Core.pdb differ diff --git a/DCIT.Core/bin/x64/Release/net48/DCIT.Core.dll b/DCIT.Core/bin/x64/Release/net48/DCIT.Core.dll new file mode 100644 index 0000000..94a22a9 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/DCIT.Core.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/DCIT.Core.pdb b/DCIT.Core/bin/x64/Release/net48/DCIT.Core.pdb new file mode 100644 index 0000000..9125556 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/DCIT.Core.pdb differ diff --git a/DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.Framework.dll b/DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.Framework.dll new file mode 100644 index 0000000..1407d6b Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.Framework.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.dll b/DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.dll new file mode 100644 index 0000000..92b3b68 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/DocumentFormat.OpenXml.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/ExCSS.dll b/DCIT.Core/bin/x64/Release/net48/ExCSS.dll new file mode 100644 index 0000000..aca855d Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/ExCSS.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/Newtonsoft.Json.dll b/DCIT.Core/bin/x64/Release/net48/Newtonsoft.Json.dll new file mode 100644 index 0000000..3f6541a Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/Newtonsoft.Json.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/Svg.dll b/DCIT.Core/bin/x64/Release/net48/Svg.dll new file mode 100644 index 0000000..b50b076 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/Svg.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/System.Buffers.dll b/DCIT.Core/bin/x64/Release/net48/System.Buffers.dll new file mode 100644 index 0000000..f2d83c5 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/System.Buffers.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/System.Memory.dll b/DCIT.Core/bin/x64/Release/net48/System.Memory.dll new file mode 100644 index 0000000..4617199 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/System.Memory.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/System.Numerics.Vectors.dll b/DCIT.Core/bin/x64/Release/net48/System.Numerics.Vectors.dll new file mode 100644 index 0000000..0865972 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/System.Numerics.Vectors.dll differ diff --git a/DCIT.Core/bin/x64/Release/net48/System.Runtime.CompilerServices.Unsafe.dll b/DCIT.Core/bin/x64/Release/net48/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000..de9e124 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net48/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.deps.json b/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.deps.json new file mode 100644 index 0000000..76320bf --- /dev/null +++ b/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.deps.json @@ -0,0 +1,194 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "DCIT.Core/1.0.0": { + "dependencies": { + "DocumentFormat.OpenXml": "3.5.1", + "Newtonsoft.Json": "13.0.4", + "Svg": "3.4.7" + }, + "runtime": { + "DCIT.Core.dll": {} + } + }, + "DocumentFormat.OpenXml/3.5.1": { + "dependencies": { + "DocumentFormat.OpenXml.Framework": "3.5.1" + }, + "runtime": { + "lib/netstandard2.0/DocumentFormat.OpenXml.dll": { + "assemblyVersion": "3.5.1.0", + "fileVersion": "3.5.1.0" + } + } + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "dependencies": { + "System.IO.Packaging": "8.0.1" + }, + "runtime": { + "lib/net6.0/DocumentFormat.OpenXml.Framework.dll": { + "assemblyVersion": "3.5.1.0", + "fileVersion": "3.5.1.0" + } + } + }, + "ExCSS/4.2.3": { + "runtime": { + "lib/net6.0/ExCSS.dll": { + "assemblyVersion": "4.2.3.0", + "fileVersion": "4.2.3.0" + } + } + }, + "Microsoft.NETCore.Platforms/5.0.0": {}, + "Microsoft.Win32.SystemEvents/5.0.0": { + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": { + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + }, + "runtimeTargets": { + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + } + }, + "Newtonsoft.Json/13.0.4": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.4.30916" + } + } + }, + "Svg/3.4.7": { + "dependencies": { + "ExCSS": "4.2.3", + "System.Drawing.Common": "5.0.3" + }, + "runtime": { + "lib/net6.0/Svg.dll": { + "assemblyVersion": "3.4.0.0", + "fileVersion": "3.4.7.1" + } + } + }, + "System.Drawing.Common/5.0.3": { + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + }, + "runtime": { + "lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + } + }, + "runtimeTargets": { + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "rid": "unix", + "assetType": "runtime", + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + }, + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "5.0.0.2", + "fileVersion": "5.0.1221.52207" + } + } + }, + "System.IO.Packaging/8.0.1": { + "runtime": { + "lib/net6.0/System.IO.Packaging.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.1024.46610" + } + } + } + } + }, + "libraries": { + "DCIT.Core/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "DocumentFormat.OpenXml/3.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==", + "path": "documentformat.openxml/3.5.1", + "hashPath": "documentformat.openxml.3.5.1.nupkg.sha512" + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==", + "path": "documentformat.openxml.framework/3.5.1", + "hashPath": "documentformat.openxml.framework.3.5.1.nupkg.sha512" + }, + "ExCSS/4.2.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==", + "path": "excss/4.2.3", + "hashPath": "excss.4.2.3.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "path": "microsoft.netcore.platforms/5.0.0", + "hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512" + }, + "Microsoft.Win32.SystemEvents/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "path": "microsoft.win32.systemevents/5.0.0", + "hashPath": "microsoft.win32.systemevents.5.0.0.nupkg.sha512" + }, + "Newtonsoft.Json/13.0.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==", + "path": "newtonsoft.json/13.0.4", + "hashPath": "newtonsoft.json.13.0.4.nupkg.sha512" + }, + "Svg/3.4.7": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==", + "path": "svg/3.4.7", + "hashPath": "svg.3.4.7.nupkg.sha512" + }, + "System.Drawing.Common/5.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==", + "path": "system.drawing.common/5.0.3", + "hashPath": "system.drawing.common.5.0.3.nupkg.sha512" + }, + "System.IO.Packaging/8.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==", + "path": "system.io.packaging/8.0.1", + "hashPath": "system.io.packaging.8.0.1.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.dll b/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.dll new file mode 100644 index 0000000..6739cb0 Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.dll differ diff --git a/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.pdb b/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.pdb new file mode 100644 index 0000000..9922f5e Binary files /dev/null and b/DCIT.Core/bin/x64/Release/net6.0-windows/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/DCIT.Core.csproj.nuget.dgspec.json b/DCIT.Core/obj/DCIT.Core.csproj.nuget.dgspec.json new file mode 100644 index 0000000..83f5ab7 --- /dev/null +++ b/DCIT.Core/obj/DCIT.Core.csproj.nuget.dgspec.json @@ -0,0 +1,77 @@ +{ + "format": 1, + "restore": { + "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj": {} + }, + "projects": { + "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj", + "projectName": "DCIT.Core", + "projectPath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj", + "packagesPath": "C:\\Users\\ly282\\.nuget\\packages\\", + "outputPath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\ly282\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net6.0-windows" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net6.0-windows7.0": { + "targetAlias": "net6.0-windows", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + } + }, + "frameworks": { + "net6.0-windows7.0": { + "targetAlias": "net6.0-windows", + "dependencies": { + "DocumentFormat.OpenXml": { + "target": "Package", + "version": "[3.5.1, )" + }, + "Newtonsoft.Json": { + "target": "Package", + "version": "[13.0.4, )" + }, + "Svg": { + "target": "Package", + "version": "[3.4.7, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\6.0.428\\RuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/DCIT.Core/obj/DCIT.Core.csproj.nuget.g.props b/DCIT.Core/obj/DCIT.Core.csproj.nuget.g.props new file mode 100644 index 0000000..0b38bdd --- /dev/null +++ b/DCIT.Core/obj/DCIT.Core.csproj.nuget.g.props @@ -0,0 +1,15 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\ly282\.nuget\packages\ + PackageReference + 6.3.4 + + + + + \ No newline at end of file diff --git a/DCIT.Core/obj/DCIT.Core.csproj.nuget.g.targets b/DCIT.Core/obj/DCIT.Core.csproj.nuget.g.targets new file mode 100644 index 0000000..3dc06ef --- /dev/null +++ b/DCIT.Core/obj/DCIT.Core.csproj.nuget.g.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/DCIT.Core/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs b/DCIT.Core/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs new file mode 100644 index 0000000..15efebf --- /dev/null +++ b/DCIT.Core/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfo.cs b/DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfo.cs new file mode 100644 index 0000000..29f7e98 --- /dev/null +++ b/DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// 由 MSBuild WriteCodeFragment 类生成。 + diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfoInputs.cache b/DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfoInputs.cache new file mode 100644 index 0000000..d5d3442 --- /dev/null +++ b/DCIT.Core/obj/Debug/net48/DCIT.Core.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +be533eba519dee59447d07ad90b6f95f12e69288 diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig b/DCIT.Core/obj/Debug/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..050914d --- /dev/null +++ b/DCIT.Core/obj/Debug/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,8 @@ +is_global = true +build_property.RootNamespace = DCIT.Core +build_property.ProjectDir = D:\projects\DCIT\src\DCIT.Core\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.CsWinRTUseWindowsUIXamlProjections = false +build_property.EffectiveAnalysisLevelStyle = +build_property.EnableCodeStyleSeverity = diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.assets.cache b/DCIT.Core/obj/Debug/net48/DCIT.Core.assets.cache new file mode 100644 index 0000000..840244a Binary files /dev/null and b/DCIT.Core/obj/Debug/net48/DCIT.Core.assets.cache differ diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.AssemblyReference.cache b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.AssemblyReference.cache new file mode 100644 index 0000000..a338bb7 Binary files /dev/null and b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.AssemblyReference.cache differ diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.CopyComplete b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.CopyComplete new file mode 100644 index 0000000..e69de29 diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.CoreCompileInputs.cache b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..3e64914 --- /dev/null +++ b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +ff20ad4232501c3bedafca0139d87ca3385ed275 diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.FileListAbsolute.txt b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..8e5841e --- /dev/null +++ b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.FileListAbsolute.txt @@ -0,0 +1,43 @@ +D:\projects\DCIT\.build\verify-core-tests\DCIT.Core.dll +D:\projects\DCIT\.build\verify-core-tests\DCIT.Core.pdb +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.AssemblyReference.cache +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfoInputs.cache +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfo.cs +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.CoreCompileInputs.cache +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.Up2Date +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.dll +D:\projects\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.pdb +D:\projects\DCIT\.build\verify-app-tests\DCIT.Core.dll +D:\projects\DCIT\.build\verify-app-tests\DCIT.Core.pdb +D:\projects\DCIT\.build\verify-app\DCIT.Core.dll +D:\projects\DCIT\.build\verify-app\DCIT.Core.pdb +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.pdb +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.Framework.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\ExCSS.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\Newtonsoft.Json.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\Svg.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Buffers.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Memory.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Numerics.Vectors.dll +D:\projects\DCIT\src\DCIT.Core\bin\Debug\net48\System.Runtime.CompilerServices.Unsafe.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DCIT.Core.pdb +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\DocumentFormat.OpenXml.Framework.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\ExCSS.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\Newtonsoft.Json.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\Svg.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Buffers.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Memory.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Numerics.Vectors.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net48\System.Runtime.CompilerServices.Unsafe.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csprojAssemblyReference.cache +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfoInputs.cache +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.AssemblyInfo.cs +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.CoreCompileInputs.cache +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.csproj.CopyComplete +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net48\DCIT.Core.pdb diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.Up2Date b/DCIT.Core/obj/Debug/net48/DCIT.Core.csproj.Up2Date new file mode 100644 index 0000000..e69de29 diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.csprojAssemblyReference.cache b/DCIT.Core/obj/Debug/net48/DCIT.Core.csprojAssemblyReference.cache new file mode 100644 index 0000000..770da2e Binary files /dev/null and b/DCIT.Core/obj/Debug/net48/DCIT.Core.csprojAssemblyReference.cache differ diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.dll b/DCIT.Core/obj/Debug/net48/DCIT.Core.dll new file mode 100644 index 0000000..fb14d3f Binary files /dev/null and b/DCIT.Core/obj/Debug/net48/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Debug/net48/DCIT.Core.pdb b/DCIT.Core/obj/Debug/net48/DCIT.Core.pdb new file mode 100644 index 0000000..c8870ce Binary files /dev/null and b/DCIT.Core/obj/Debug/net48/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/DCIT.Core/obj/Debug/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 0000000..36203c7 --- /dev/null +++ b/DCIT.Core/obj/Debug/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfo.cs b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfo.cs new file mode 100644 index 0000000..9473790 --- /dev/null +++ b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfo.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] +[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")] +[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")] + +// 由 MSBuild WriteCodeFragment 类生成。 + diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache new file mode 100644 index 0000000..3fc80f5 --- /dev/null +++ b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +8c9305df27e0b0d043e93bc957ca166b518cd300 diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..d75d178 --- /dev/null +++ b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,10 @@ +is_global = true +build_property.TargetFramework = net6.0-windows +build_property.TargetPlatformMinVersion = 7.0 +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = DCIT.Core +build_property.ProjectDir = E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.assets.cache b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.assets.cache new file mode 100644 index 0000000..81ef28d Binary files /dev/null and b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.assets.cache differ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache new file mode 100644 index 0000000..49a7b0b Binary files /dev/null and b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache differ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..e89f18a --- /dev/null +++ b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +fed07599d218f093230e61661b1a9eee3dc52606 diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..b420389 --- /dev/null +++ b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt @@ -0,0 +1,12 @@ +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net6.0-windows\DCIT.Core.deps.json +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net6.0-windows\DCIT.Core.dll +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Debug\net6.0-windows\DCIT.Core.pdb +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.csproj.AssemblyReference.cache +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.AssemblyInfoInputs.cache +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.AssemblyInfo.cs +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.csproj.CoreCompileInputs.cache +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.dll +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\refint\DCIT.Core.dll +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\DCIT.Core.pdb +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Debug\net6.0-windows\ref\DCIT.Core.dll diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.dll b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.dll new file mode 100644 index 0000000..ce6fde6 Binary files /dev/null and b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.pdb b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.pdb new file mode 100644 index 0000000..21c4915 Binary files /dev/null and b/DCIT.Core/obj/Debug/net6.0-windows/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/ref/DCIT.Core.dll b/DCIT.Core/obj/Debug/net6.0-windows/ref/DCIT.Core.dll new file mode 100644 index 0000000..09d9f31 Binary files /dev/null and b/DCIT.Core/obj/Debug/net6.0-windows/ref/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Debug/net6.0-windows/refint/DCIT.Core.dll b/DCIT.Core/obj/Debug/net6.0-windows/refint/DCIT.Core.dll new file mode 100644 index 0000000..09d9f31 Binary files /dev/null and b/DCIT.Core/obj/Debug/net6.0-windows/refint/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Release/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs b/DCIT.Core/obj/Release/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs new file mode 100644 index 0000000..15efebf --- /dev/null +++ b/DCIT.Core/obj/Release/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.AssemblyInfo.cs b/DCIT.Core/obj/Release/net48/DCIT.Core.AssemblyInfo.cs new file mode 100644 index 0000000..ce0035a --- /dev/null +++ b/DCIT.Core/obj/Release/net48/DCIT.Core.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// 由 MSBuild WriteCodeFragment 类生成。 + diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.AssemblyInfoInputs.cache b/DCIT.Core/obj/Release/net48/DCIT.Core.AssemblyInfoInputs.cache new file mode 100644 index 0000000..9b584dc --- /dev/null +++ b/DCIT.Core/obj/Release/net48/DCIT.Core.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +d434416be4a988aa0cdcfa517f06e07a43042ffd0051a197e99d237b1ad7199d diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig b/DCIT.Core/obj/Release/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..050914d --- /dev/null +++ b/DCIT.Core/obj/Release/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,8 @@ +is_global = true +build_property.RootNamespace = DCIT.Core +build_property.ProjectDir = D:\projects\DCIT\src\DCIT.Core\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.CsWinRTUseWindowsUIXamlProjections = false +build_property.EffectiveAnalysisLevelStyle = +build_property.EnableCodeStyleSeverity = diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.assets.cache b/DCIT.Core/obj/Release/net48/DCIT.Core.assets.cache new file mode 100644 index 0000000..386ce7b Binary files /dev/null and b/DCIT.Core/obj/Release/net48/DCIT.Core.assets.cache differ diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.AssemblyReference.cache b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.AssemblyReference.cache new file mode 100644 index 0000000..dbc7352 Binary files /dev/null and b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.AssemblyReference.cache differ diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.CoreCompileInputs.cache b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..d76340d --- /dev/null +++ b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +33f62fb6c93189a04328821497b352b8bb66e83ae25b87bea08ae8ef4905e2b8 diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.FileListAbsolute.txt b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..16e2b26 --- /dev/null +++ b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.FileListAbsolute.txt @@ -0,0 +1,19 @@ +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\DCIT.Core.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\DCIT.Core.pdb +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\DocumentFormat.OpenXml.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\DocumentFormat.OpenXml.Framework.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\ExCSS.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\Newtonsoft.Json.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\Svg.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\System.Buffers.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\System.Memory.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\System.Numerics.Vectors.dll +D:\projects\DCIT\src\DCIT.Core\bin\Release\net48\System.Runtime.CompilerServices.Unsafe.dll +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.csproj.AssemblyReference.cache +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.AssemblyInfoInputs.cache +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.AssemblyInfo.cs +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.csproj.CoreCompileInputs.cache +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.csproj.Up2Date +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.dll +D:\projects\DCIT\src\DCIT.Core\obj\Release\net48\DCIT.Core.pdb diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.Up2Date b/DCIT.Core/obj/Release/net48/DCIT.Core.csproj.Up2Date new file mode 100644 index 0000000..e69de29 diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.dll b/DCIT.Core/obj/Release/net48/DCIT.Core.dll new file mode 100644 index 0000000..ae58421 Binary files /dev/null and b/DCIT.Core/obj/Release/net48/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Release/net48/DCIT.Core.pdb b/DCIT.Core/obj/Release/net48/DCIT.Core.pdb new file mode 100644 index 0000000..84baef7 Binary files /dev/null and b/DCIT.Core/obj/Release/net48/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/Release/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/DCIT.Core/obj/Release/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 0000000..36203c7 --- /dev/null +++ b/DCIT.Core/obj/Release/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.AssemblyInfo.cs b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.AssemblyInfo.cs new file mode 100644 index 0000000..1d04471 --- /dev/null +++ b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.AssemblyInfo.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] +[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")] +[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")] + +// 由 MSBuild WriteCodeFragment 类生成。 + diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache new file mode 100644 index 0000000..02236cc --- /dev/null +++ b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +2954f64ad115a6a4ec128884b807e9eac2d1fe52 diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..c368e5e --- /dev/null +++ b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,13 @@ +is_global = true +build_property.TargetFramework = net6.0-windows +build_property.TargetPlatformMinVersion = 7.0 +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.EnableSingleFileAnalyzer = true +build_property.EnableTrimAnalyzer = +build_property.IncludeAllContentForSelfExtract = +build_property.RootNamespace = DCIT.Core +build_property.ProjectDir = E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\ diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.assets.cache b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.assets.cache new file mode 100644 index 0000000..542f542 Binary files /dev/null and b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.assets.cache differ diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache new file mode 100644 index 0000000..49a7b0b Binary files /dev/null and b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache differ diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..1d80783 --- /dev/null +++ b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +5e99970d9824267e30c4d24d74cc1acf17c3b449 diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..74e143a --- /dev/null +++ b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt @@ -0,0 +1,24 @@ +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\bin\Release\net6.0-windows\DCIT.Core.deps.json +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\bin\Release\net6.0-windows\DCIT.Core.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\bin\Release\net6.0-windows\DCIT.Core.pdb +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.csproj.AssemblyReference.cache +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.AssemblyInfoInputs.cache +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.AssemblyInfo.cs +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.csproj.CoreCompileInputs.cache +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\refint\DCIT.Core.dll +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.pdb +E:\projects\wxwx\projects_job\一般需求项目\WORD文档替换与还原工具\codex\DCIT\历史版本\20260625\src\DCIT.Core\obj\Release\net6.0-windows\ref\DCIT.Core.dll +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Release\net6.0-windows\DCIT.Core.deps.json +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Release\net6.0-windows\DCIT.Core.dll +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\Release\net6.0-windows\DCIT.Core.pdb +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.csproj.AssemblyReference.cache +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.AssemblyInfoInputs.cache +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.AssemblyInfo.cs +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.csproj.CoreCompileInputs.cache +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.dll +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\refint\DCIT.Core.dll +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\DCIT.Core.pdb +e:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\Release\net6.0-windows\ref\DCIT.Core.dll diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.dll b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.dll new file mode 100644 index 0000000..6c659e5 Binary files /dev/null and b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.pdb b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.pdb new file mode 100644 index 0000000..7c317a5 Binary files /dev/null and b/DCIT.Core/obj/Release/net6.0-windows/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/Release/net6.0-windows/ref/DCIT.Core.dll b/DCIT.Core/obj/Release/net6.0-windows/ref/DCIT.Core.dll new file mode 100644 index 0000000..c4affb9 Binary files /dev/null and b/DCIT.Core/obj/Release/net6.0-windows/ref/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/Release/net6.0-windows/refint/DCIT.Core.dll b/DCIT.Core/obj/Release/net6.0-windows/refint/DCIT.Core.dll new file mode 100644 index 0000000..c4affb9 Binary files /dev/null and b/DCIT.Core/obj/Release/net6.0-windows/refint/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/project.assets.json b/DCIT.Core/obj/project.assets.json new file mode 100644 index 0000000..03a702f --- /dev/null +++ b/DCIT.Core/obj/project.assets.json @@ -0,0 +1,477 @@ +{ + "version": 3, + "targets": { + "net6.0-windows7.0": { + "DocumentFormat.OpenXml/3.5.1": { + "type": "package", + "dependencies": { + "DocumentFormat.OpenXml.Framework": "3.5.1" + }, + "compile": { + "lib/netstandard2.0/DocumentFormat.OpenXml.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/DocumentFormat.OpenXml.dll": { + "related": ".xml" + } + } + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "type": "package", + "dependencies": { + "System.IO.Packaging": "8.0.1" + }, + "compile": { + "lib/net6.0/DocumentFormat.OpenXml.Framework.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/DocumentFormat.OpenXml.Framework.dll": { + "related": ".xml" + } + } + }, + "ExCSS/4.2.3": { + "type": "package", + "compile": { + "lib/net6.0/ExCSS.dll": {} + }, + "runtime": { + "lib/net6.0/ExCSS.dll": {} + } + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "compile": { + "lib/netstandard1.0/_._": {} + }, + "runtime": { + "lib/netstandard1.0/_._": {} + } + }, + "Microsoft.Win32.SystemEvents/5.0.0": { + "type": "package", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + }, + "compile": { + "ref/netstandard2.0/_._": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": { + "related": ".xml" + } + }, + "runtimeTargets": { + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, + "Newtonsoft.Json/13.0.4": { + "type": "package", + "compile": { + "lib/net6.0/Newtonsoft.Json.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "related": ".xml" + } + } + }, + "Svg/3.4.7": { + "type": "package", + "dependencies": { + "ExCSS": "4.2.3", + "System.Drawing.Common": "5.0.3" + }, + "compile": { + "lib/net6.0/Svg.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/Svg.dll": { + "related": ".xml" + } + } + }, + "System.Drawing.Common/5.0.3": { + "type": "package", + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + }, + "compile": { + "ref/netcoreapp3.0/System.Drawing.Common.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netcoreapp3.0/System.Drawing.Common.dll": { + "related": ".xml" + } + }, + "runtimeTargets": { + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assetType": "runtime", + "rid": "unix" + }, + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, + "System.IO.Packaging/8.0.1": { + "type": "package", + "compile": { + "lib/net6.0/System.IO.Packaging.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/System.IO.Packaging.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net6.0/_._": {} + } + } + } + }, + "libraries": { + "DocumentFormat.OpenXml/3.5.1": { + "sha512": "zxdOf5VVCe/uNklbRhj8dVBzQGj3DoqkUuqOp9cAZVuN8mNYDjof1lvSQA2OQNr8Ptc9d7pbA7Azq/ReaI3FpA==", + "type": "package", + "path": "documentformat.openxml/3.5.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "documentformat.openxml.3.5.1.nupkg.sha512", + "documentformat.openxml.nuspec", + "icon.png", + "lib/net10.0/DocumentFormat.OpenXml.dll", + "lib/net10.0/DocumentFormat.OpenXml.xml", + "lib/net35/DocumentFormat.OpenXml.dll", + "lib/net35/DocumentFormat.OpenXml.xml", + "lib/net40/DocumentFormat.OpenXml.dll", + "lib/net40/DocumentFormat.OpenXml.xml", + "lib/net46/DocumentFormat.OpenXml.dll", + "lib/net46/DocumentFormat.OpenXml.xml", + "lib/net8.0/DocumentFormat.OpenXml.dll", + "lib/net8.0/DocumentFormat.OpenXml.xml", + "lib/netstandard2.0/DocumentFormat.OpenXml.dll", + "lib/netstandard2.0/DocumentFormat.OpenXml.xml" + ] + }, + "DocumentFormat.OpenXml.Framework/3.5.1": { + "sha512": "U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==", + "type": "package", + "path": "documentformat.openxml.framework/3.5.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "documentformat.openxml.framework.3.5.1.nupkg.sha512", + "documentformat.openxml.framework.nuspec", + "icon.png", + "lib/net10.0/DocumentFormat.OpenXml.Framework.dll", + "lib/net10.0/DocumentFormat.OpenXml.Framework.xml", + "lib/net35/DocumentFormat.OpenXml.Framework.dll", + "lib/net35/DocumentFormat.OpenXml.Framework.xml", + "lib/net40/DocumentFormat.OpenXml.Framework.dll", + "lib/net40/DocumentFormat.OpenXml.Framework.xml", + "lib/net46/DocumentFormat.OpenXml.Framework.dll", + "lib/net46/DocumentFormat.OpenXml.Framework.xml", + "lib/net6.0/DocumentFormat.OpenXml.Framework.dll", + "lib/net6.0/DocumentFormat.OpenXml.Framework.xml", + "lib/net8.0/DocumentFormat.OpenXml.Framework.dll", + "lib/net8.0/DocumentFormat.OpenXml.Framework.xml", + "lib/netstandard2.0/DocumentFormat.OpenXml.Framework.dll", + "lib/netstandard2.0/DocumentFormat.OpenXml.Framework.xml" + ] + }, + "ExCSS/4.2.3": { + "sha512": "SyeAfu2wL5247sipJoPUzQfjiwQtfSd8hN4IbgoyVcDx4PP6Dud4znwPRibWQzLtTlUxYYcbf5f4p+EfFC7KtQ==", + "type": "package", + "path": "excss/4.2.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "excss.4.2.3.nupkg.sha512", + "excss.nuspec", + "lib/net48/ExCSS.dll", + "lib/net6.0/ExCSS.dll", + "lib/net7.0/ExCSS.dll", + "lib/netcoreapp3.1/ExCSS.dll", + "lib/netstandard2.0/ExCSS.dll", + "lib/netstandard2.1/ExCSS.dll", + "readme.md" + ] + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "sha512": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "type": "package", + "path": "microsoft.netcore.platforms/5.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/netstandard1.0/_._", + "microsoft.netcore.platforms.5.0.0.nupkg.sha512", + "microsoft.netcore.platforms.nuspec", + "runtime.json", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "Microsoft.Win32.SystemEvents/5.0.0": { + "sha512": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "type": "package", + "path": "microsoft.win32.systemevents/5.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/net461/Microsoft.Win32.SystemEvents.dll", + "lib/net461/Microsoft.Win32.SystemEvents.xml", + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll", + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.xml", + "microsoft.win32.systemevents.5.0.0.nupkg.sha512", + "microsoft.win32.systemevents.nuspec", + "ref/net461/Microsoft.Win32.SystemEvents.dll", + "ref/net461/Microsoft.Win32.SystemEvents.xml", + "ref/netstandard2.0/Microsoft.Win32.SystemEvents.dll", + "ref/netstandard2.0/Microsoft.Win32.SystemEvents.xml", + "runtimes/win/lib/netcoreapp2.0/Microsoft.Win32.SystemEvents.dll", + "runtimes/win/lib/netcoreapp2.0/Microsoft.Win32.SystemEvents.xml", + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll", + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.xml", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "Newtonsoft.Json/13.0.4": { + "sha512": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==", + "type": "package", + "path": "newtonsoft.json/13.0.4", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.md", + "README.md", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/net6.0/Newtonsoft.Json.dll", + "lib/net6.0/Newtonsoft.Json.xml", + "lib/netstandard1.0/Newtonsoft.Json.dll", + "lib/netstandard1.0/Newtonsoft.Json.xml", + "lib/netstandard1.3/Newtonsoft.Json.dll", + "lib/netstandard1.3/Newtonsoft.Json.xml", + "lib/netstandard2.0/Newtonsoft.Json.dll", + "lib/netstandard2.0/Newtonsoft.Json.xml", + "newtonsoft.json.13.0.4.nupkg.sha512", + "newtonsoft.json.nuspec", + "packageIcon.png" + ] + }, + "Svg/3.4.7": { + "sha512": "Omez7ly5BGhg3OzdV+LHZ5sI0+JQ6hF7WVKUeyHw4jRvcEWNCPCf1MWMBaf+R0DRBSZHx5EUHwBTEF+2oYtsAw==", + "type": "package", + "path": "svg/3.4.7", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net462/Svg.dll", + "lib/net462/Svg.xml", + "lib/net472/Svg.dll", + "lib/net472/Svg.xml", + "lib/net481/Svg.dll", + "lib/net481/Svg.xml", + "lib/net6.0/Svg.dll", + "lib/net6.0/Svg.xml", + "lib/net8.0/Svg.dll", + "lib/net8.0/Svg.xml", + "lib/netcoreapp3.1/Svg.dll", + "lib/netcoreapp3.1/Svg.xml", + "lib/netstandard2.0/Svg.dll", + "lib/netstandard2.0/Svg.xml", + "lib/netstandard2.1/Svg.dll", + "lib/netstandard2.1/Svg.xml", + "svg-logo-v.png", + "svg.3.4.7.nupkg.sha512", + "svg.nuspec" + ] + }, + "System.Drawing.Common/5.0.3": { + "sha512": "rEQZuslijqdsO0pkJn7LtGBaMc//YVA8de0meGihkg9oLPaN+w+/Pb5d71lgp0YjPoKgBKNMvdq0IPnoW4PEng==", + "type": "package", + "path": "system.drawing.common/5.0.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net461/System.Drawing.Common.dll", + "lib/netcoreapp3.0/System.Drawing.Common.dll", + "lib/netcoreapp3.0/System.Drawing.Common.xml", + "lib/netstandard2.0/System.Drawing.Common.dll", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net461/System.Drawing.Common.dll", + "ref/netcoreapp3.0/System.Drawing.Common.dll", + "ref/netcoreapp3.0/System.Drawing.Common.xml", + "ref/netstandard2.0/System.Drawing.Common.dll", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "runtimes/unix/lib/netcoreapp2.0/System.Drawing.Common.dll", + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll", + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.xml", + "runtimes/win/lib/netcoreapp2.0/System.Drawing.Common.dll", + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll", + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.xml", + "system.drawing.common.5.0.3.nupkg.sha512", + "system.drawing.common.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.IO.Packaging/8.0.1": { + "sha512": "KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==", + "type": "package", + "path": "system.io.packaging/8.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/System.IO.Packaging.targets", + "buildTransitive/net462/_._", + "buildTransitive/net6.0/_._", + "buildTransitive/netcoreapp2.0/System.IO.Packaging.targets", + "lib/net462/System.IO.Packaging.dll", + "lib/net462/System.IO.Packaging.xml", + "lib/net6.0/System.IO.Packaging.dll", + "lib/net6.0/System.IO.Packaging.xml", + "lib/net7.0/System.IO.Packaging.dll", + "lib/net7.0/System.IO.Packaging.xml", + "lib/net8.0/System.IO.Packaging.dll", + "lib/net8.0/System.IO.Packaging.xml", + "lib/netstandard2.0/System.IO.Packaging.dll", + "lib/netstandard2.0/System.IO.Packaging.xml", + "system.io.packaging.8.0.1.nupkg.sha512", + "system.io.packaging.nuspec", + "useSharedDesignerContext.txt" + ] + } + }, + "projectFileDependencyGroups": { + "net6.0-windows7.0": [ + "DocumentFormat.OpenXml >= 3.5.1", + "Newtonsoft.Json >= 13.0.4", + "Svg >= 3.4.7" + ] + }, + "packageFolders": { + "C:\\Users\\ly282\\.nuget\\packages\\": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj", + "projectName": "DCIT.Core", + "projectPath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj", + "packagesPath": "C:\\Users\\ly282\\.nuget\\packages\\", + "outputPath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\ly282\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net6.0-windows" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net6.0-windows7.0": { + "targetAlias": "net6.0-windows", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + } + }, + "frameworks": { + "net6.0-windows7.0": { + "targetAlias": "net6.0-windows", + "dependencies": { + "DocumentFormat.OpenXml": { + "target": "Package", + "version": "[3.5.1, )" + }, + "Newtonsoft.Json": { + "target": "Package", + "version": "[13.0.4, )" + }, + "Svg": { + "target": "Package", + "version": "[3.4.7, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\6.0.428\\RuntimeIdentifierGraph.json" + } + } + } +} \ No newline at end of file diff --git a/DCIT.Core/obj/project.nuget.cache b/DCIT.Core/obj/project.nuget.cache new file mode 100644 index 0000000..a259a48 --- /dev/null +++ b/DCIT.Core/obj/project.nuget.cache @@ -0,0 +1,18 @@ +{ + "version": 2, + "dgSpecHash": "O7dpA6ALy8zfyKGD+JOIb5nvvU6ozZNfGmXGe9YAp1j0OARRbgluG+EAxV9zXR80g9YnbY98adSKn9JFfeYfpQ==", + "success": true, + "projectFilePath": "E:\\projects\\wxwx\\projects_job\\GeneralRequirement\\WORD文档替换与还原工具\\codex\\DCIT\\src\\DCIT.Core\\DCIT.Core.csproj", + "expectedPackageFiles": [ + "C:\\Users\\ly282\\.nuget\\packages\\documentformat.openxml\\3.5.1\\documentformat.openxml.3.5.1.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\documentformat.openxml.framework\\3.5.1\\documentformat.openxml.framework.3.5.1.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\excss\\4.2.3\\excss.4.2.3.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\microsoft.netcore.platforms\\5.0.0\\microsoft.netcore.platforms.5.0.0.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\microsoft.win32.systemevents\\5.0.0\\microsoft.win32.systemevents.5.0.0.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\newtonsoft.json\\13.0.4\\newtonsoft.json.13.0.4.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\svg\\3.4.7\\svg.3.4.7.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\system.drawing.common\\5.0.3\\system.drawing.common.5.0.3.nupkg.sha512", + "C:\\Users\\ly282\\.nuget\\packages\\system.io.packaging\\8.0.1\\system.io.packaging.8.0.1.nupkg.sha512" + ], + "logs": [] +} \ No newline at end of file diff --git a/DCIT.Core/obj/x64/Release/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs b/DCIT.Core/obj/x64/Release/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs new file mode 100644 index 0000000..15efebf --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.AssemblyInfo.cs b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.AssemblyInfo.cs new file mode 100644 index 0000000..ce0035a --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// 由 MSBuild WriteCodeFragment 类生成。 + diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.AssemblyInfoInputs.cache b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.AssemblyInfoInputs.cache new file mode 100644 index 0000000..9b584dc --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +d434416be4a988aa0cdcfa517f06e07a43042ffd0051a197e99d237b1ad7199d diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..050914d --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,8 @@ +is_global = true +build_property.RootNamespace = DCIT.Core +build_property.ProjectDir = D:\projects\DCIT\src\DCIT.Core\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.CsWinRTUseWindowsUIXamlProjections = false +build_property.EffectiveAnalysisLevelStyle = +build_property.EnableCodeStyleSeverity = diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.assets.cache b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.assets.cache new file mode 100644 index 0000000..c75ee18 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.assets.cache differ diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.AssemblyReference.cache b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.AssemblyReference.cache new file mode 100644 index 0000000..dbc7352 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.AssemblyReference.cache differ diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.CoreCompileInputs.cache b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..1cac09f --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +f45b6befabe0e8759b85c6a81c76a33f0f3336e716f5844a2eb7f78781f2895c diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.FileListAbsolute.txt b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..67d7f01 --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.FileListAbsolute.txt @@ -0,0 +1,19 @@ +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\DCIT.Core.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\DCIT.Core.pdb +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\DocumentFormat.OpenXml.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\DocumentFormat.OpenXml.Framework.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\ExCSS.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\Newtonsoft.Json.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\Svg.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\System.Buffers.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\System.Memory.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\System.Numerics.Vectors.dll +D:\projects\DCIT\src\DCIT.Core\bin\x64\Release\net48\System.Runtime.CompilerServices.Unsafe.dll +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.csproj.AssemblyReference.cache +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.AssemblyInfoInputs.cache +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.AssemblyInfo.cs +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.csproj.CoreCompileInputs.cache +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.csproj.Up2Date +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.dll +D:\projects\DCIT\src\DCIT.Core\obj\x64\Release\net48\DCIT.Core.pdb diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.Up2Date b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.csproj.Up2Date new file mode 100644 index 0000000..e69de29 diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.dll b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.dll new file mode 100644 index 0000000..94a22a9 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/x64/Release/net48/DCIT.Core.pdb b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.pdb new file mode 100644 index 0000000..9125556 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net48/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/DCIT.Core/obj/x64/Release/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 0000000..36203c7 --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.AssemblyInfo.cs b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.AssemblyInfo.cs new file mode 100644 index 0000000..1d04471 --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.AssemblyInfo.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyTitleAttribute("DCIT.Core")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] +[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")] +[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")] + +// 由 MSBuild WriteCodeFragment 类生成。 + diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache new file mode 100644 index 0000000..02236cc --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +2954f64ad115a6a4ec128884b807e9eac2d1fe52 diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..d75d178 --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,10 @@ +is_global = true +build_property.TargetFramework = net6.0-windows +build_property.TargetPlatformMinVersion = 7.0 +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = DCIT.Core +build_property.ProjectDir = E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.assets.cache b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.assets.cache new file mode 100644 index 0000000..a3428e1 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.assets.cache differ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache new file mode 100644 index 0000000..49a7b0b Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.AssemblyReference.cache differ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..3eb32af --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +4bbef03dbb815f7369e08da35be4bf81f8b2ce3d diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..1a46ff7 --- /dev/null +++ b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.csproj.FileListAbsolute.txt @@ -0,0 +1,12 @@ +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\x64\Release\net6.0-windows\DCIT.Core.deps.json +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\x64\Release\net6.0-windows\DCIT.Core.dll +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\bin\x64\Release\net6.0-windows\DCIT.Core.pdb +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.csproj.AssemblyReference.cache +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.GeneratedMSBuildEditorConfig.editorconfig +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.AssemblyInfoInputs.cache +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.AssemblyInfo.cs +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.csproj.CoreCompileInputs.cache +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.dll +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\refint\DCIT.Core.dll +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\DCIT.Core.pdb +E:\projects\wxwx\projects_job\GeneralRequirement\WORD文档替换与还原工具\codex\DCIT\src\DCIT.Core\obj\x64\Release\net6.0-windows\ref\DCIT.Core.dll diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.dll b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.dll new file mode 100644 index 0000000..6739cb0 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.pdb b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.pdb new file mode 100644 index 0000000..9922f5e Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net6.0-windows/DCIT.Core.pdb differ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/ref/DCIT.Core.dll b/DCIT.Core/obj/x64/Release/net6.0-windows/ref/DCIT.Core.dll new file mode 100644 index 0000000..e9b8a55 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net6.0-windows/ref/DCIT.Core.dll differ diff --git a/DCIT.Core/obj/x64/Release/net6.0-windows/refint/DCIT.Core.dll b/DCIT.Core/obj/x64/Release/net6.0-windows/refint/DCIT.Core.dll new file mode 100644 index 0000000..e9b8a55 Binary files /dev/null and b/DCIT.Core/obj/x64/Release/net6.0-windows/refint/DCIT.Core.dll differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6728d7 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# WORD 2007 格式调整软件 — 使用说明 + +## 简介 + +本软件用于对 Word 文档(`.doc` / `.docx`)中的文字与图片进行批量替换,并可基于替换后的文件严格还原为原始文件。适用于需要批量脱敏、格式调整、信息替换的场景。 + +> **运行环境要求**:电脑需安装 Microsoft Word 或 WPS(支持 `.docx` 格式),软件为绿色免安装版本。 + +--- + +## 一、界面概览 + +软件主界面分为三个标签页: + +| 标签页 | 功能 | +|--------|------| +| **替换** | 扫描原始文档,批量执行文字/图片替换,输出替换后文件和差异文件 | +| **恢复** | 基于替换后文件和差异文件,还原为原始文件 | +| **设置** | 管理替换规则、配置图片替换开关、自动提取参数等 | + +界面底部为**运行日志**区域,实时显示处理进度、命中次数和异常信息。 + +--- + +## 二、替换流程 + +### 2.1 基本步骤 + +1. 点击 **"选择原始文件夹"**,选择包含待处理 Word 文档的目录 +2. 点击 **"选择替换文件夹"**,选择替换后文件的输出目录 +3. 在文件列表中**勾选**需要处理的文件(支持全选/全不选) +4. 在设置页面**配置替换规则**(详见第四章) +5. 点击 **"开始替换"** + +### 2.2 批量处理 + +- 软件会自动扫描原始文件夹及其**所有子目录**中的 `.doc` 和 `.docx` 文件 +- 输出目录会**完整保留**原始文件夹的目录结构 +- 仅处理**勾选**的文件,未勾选的文件会被跳过 + +### 2.3 输出文件 + +替换后,每个文件会生成三个输出: + +| 文件类型 | 示例文件名 | 说明 | +|----------|-----------|------| +| 替换后文档 | `文件基本名-20260402.docx` | 替换后的 Word 文档 | +| 差异文件 | `文件基本名-20260402.diff` | 记录替换操作的 JSON 文件,用于恢复 | +| 差异位图 | `文件基本名-20260402.bmp` | 差异文件的位图备份,用于恢复 | + +> 文件名中的日期格式为 `yyyyMMdd`(如 `20260402` 表示 2026 年 4 月 2 日)。 + +### 2.4 自动提取规则 + +点击 **"自动提取"** 按钮,软件会自动从勾选的文档中提取高频关键字、高频词和高频句,生成候选替换规则。提取完成后会弹出预览窗口,您可以选择需要的条目追加到规则列表中。 + +--- + +## 三、恢复流程 + +### 3.1 基本步骤 + +1. 切换到 **"恢复"** 标签页 +2. 点击 **"选择替换文件夹"**,选择包含替换后文档和 `.diff` 文件的目录 +3. 点击 **"选择恢复文件夹"**,选择恢复后文件的输出目录 +4. 在文件列表中勾选需要恢复的文件 +5. 点击 **"开始恢复"** + +### 3.2 恢复原理 + +软件根据 `.diff` 文件中记录的每一次替换操作,将替换后的文档**严格还原**为原始内容。恢复后文件应与原始文件内容一致。 + +--- + +## 四、替换规则管理 + +### 4.1 规则列表 + +在 **"设置"** 标签页中管理替换规则。每条规则至少包含以下字段: + +| 字段 | 说明 | +|------|------| +| 启用 | 勾选后该规则生效 | +| 规则名称 | 便于识别的名称 | +| 匹配模式 | **普通文本** 或 **正则表达式** | +| 区分大小写 | 匹配时是否区分英文大小写 | +| 全字匹配 | 是否仅匹配完整单词 | +| 原文本 | 需要被替换的文字 | +| 新文本 | 替换后的文字 | +| 备注 | 补充说明 | + +### 4.2 规则执行顺序 + +- 规则按列表**从上到下**依次执行 +- 前一条规则替换后的结果**可被后续规则再次命中** +- 正则模式支持捕获组和回填(如 `$1`、`$2`) + +### 4.3 规则文件 + +- 点击 **"打开规则文件"** 加载已有的 `.rule` 文件 +- 点击 **"保存规则文件"** 将当前规则列表保存为 `.rule` 文件 +- 替换执行时使用当前加载的规则列表 + +### 4.4 文字替换范围 + +替换规则作用于文档的全文范围,包括: + +- 正文段落、表格、文本框 +- 页眉、页脚 +- 批注、题注、超链接 +- 目录、域结果、公式 +- 图表中的文字 + +--- + +## 五、设置说明 + +在 **"设置"** 标签页中可配置以下选项: + +### 5.1 替换相关 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| JSON 差异文件加密 | 开启 | 对 `.diff` 文件进行加密存储 | +| 启用图片替换 | 开启 | 关闭后替换流程仅处理文字,不替换图片 | +| 使用固定图像随机种子 | 关闭 | 开启后可指定种子值,使图片替换结果可复现 | + +### 5.2 自动提取参数 + +| 选项 | 说明 | +|------|------| +| 提取关键字 | 从文档中提取高频关键字 | +| 提取高频词 | 提取出现频率最高的词语 | +| 提取高频句 | 提取出现频率最高的句子 | +| 关键字算法 | 词频 / TF-IDF / TextRank(可多选) | +| 统计中文词 / 英文词 | 高频词统计的语言范围 | +| 统计中文句 / 英文句 | 高频句统计的语言范围 | +| 中文 / 英文最小长度 | 参与统计的词最小字符数 | +| 句子分隔符 | 用于切分句子的标点符号 | +| 排除邮箱地址 | 提取时是否忽略邮箱地址 | + +--- + +## 六、注意事项 + +1. **不允许跨段落匹配**:替换规则不会跨越段落边界匹配文字。 +2. **不允许跨表格单元格匹配**:表格每个单元格内的文字独立匹配。 +3. **不支持加密文档**:受密码保护的 Word 文档无法处理。 +4. **样式保持**:文字替换后,原文档的字体、字号、颜色、段落格式等样式保持不变。 +5. **占位对齐**:当新文本长度与原文本不一致时,软件会自动截断或填充,以保持原有版式。 +6. **同名冲突处理**:输出目录中如存在同名文件,会自动追加序号(如 `_1`),不会覆盖已有文件。 + +--- + +## 七、快捷操作 + +| 操作 | 方式 | +|------|------| +| 全选文件 | 点击文件列表下方的 **"全选"** 按钮 | +| 取消全选 | 点击 **"全不选"** 按钮 | +| 停止任务 | 替换/恢复执行中可点击 **"停止"** 按钮 | +| 撤销自动提取 | 自动提取追加规则后,可点击 **"撤销自动提取"** 按钮回退 | +| 清空日志 | 右键日志区域,选择清空 | \ No newline at end of file diff --git a/srs.md b/srs.md new file mode 100644 index 0000000..1c09201 --- /dev/null +++ b/srs.md @@ -0,0 +1,2820 @@ +# WORD 文件信息替换与还原工具需求规格说明书 + +## 1. 文档目的 + +本文档用于明确“WORD 文件信息替换与还原工具”的功能范围、交互方式、数据格式、兼容性约束、运行约束与验收标准,作为开发、测试和验收的统一依据。 + +## 2. 产品目标 + +本工具用于对 Word 文件中的文字与图像信息进行批量替换,并生成可用于严格还原的差异文件;随后可基于替换后的文件和差异文件恢复出还原文件。 + +工具应满足以下总体目标: + +- 同时支持 `*.doc` 和 `*.docx` 输入文件。 +- 替换和恢复宿主机必须已安装支持 `*.docx` 的 Word 或 WPS;除该明确前提外,程序及其依赖应绿色离线交付。 +- 所有依赖离线打包,目标环境无需联网下载。 +- 替换和还原过程静默执行,不显示 Word 打开界面。 +- 支持批量处理目录及子目录中的文件。 +- 还原过程采用严格匹配,不允许模糊恢复。 + +说明: + +- `*.docx` 是优先设计、优先优化、优先验证的格式。 +- `*.doc` 作为兼容性支持对象,在复杂对象场景下以最终验收结果为准。 + +## 3. 术语定义 + +- 原始文件 `[A]`:待执行替换操作的 Word 文件。 +- 替换后文件 `[B]`:执行替换操作后生成的新 Word 文件。 +- 差异文件 `[C]`:记录替换过程所需恢复信息的数据文件。 +- 还原文件 `[A']`:根据 `[B]` 和 `[C]` 还原生成的 Word 文件。 +- 规则文件:后缀为 `*.rule` 的 JSON 文件,用于定义文字替换规则。 +- Story:Word 的逻辑文本区域,例如正文、页眉、页脚、批注、文本框等。 +- Run:Word 在同一段落或容器内对连续文字进行的内部样式片段划分。视觉上连续的文字可能由多个 Run 组成。 + +## 4. 功能概述 + +系统包含以下三个页面: + +- 替换 +- 恢复 +- 设置 + +系统主要能力包括: + +- 选择原始文件目录并批量生成替换后文件与差异文件 +- 基于替换后文件和差异文件生成还原文件 +- 管理规则文件,配置文字替换规则 +- 对规则进行合法性和兼容性检查 +- 记录详细运行日志、会话配置和处理结果 + +## 5. 处理对象范围 + +### 5.1 总体范围 + +替换与恢复范围为全文范围,包含但不限于: + +- 页眉 +- 页脚 +- 正文段落 +- 正文中的表格 +- 页眉和页脚中的表格 +- 正文中的文本框 +- 页眉和页脚中的文本框 +- 批注 +- 题注 +- 超链接显示文字 +- 目录显示结果 +- 域显示结果 +- 公式中的可解析文字内容 +- 图表中的文本内容 +- 图片对象 +- 形状对象中的文字及可转换图形内容 + +### 5.2 图片对象范围 + +以下图片对象在替换与恢复范围内: + +- 嵌入式图片 +- 浮动图片 +- 页眉页脚中的图片 +- 表格单元格中的图片 +- 文本框中的图片 + +### 5.3 表格支持范围 + +表格处理要求如下: + +- 支持普通表格 +- 支持合并单元格 +- 支持表格单元格中的文字 +- 支持表格单元格中的图片 +- 不支持跨单元格匹配文字 + +### 5.4 明确不支持的处理方式 + +- 不支持跨段落匹配文字 +- 不支持对加密文档进行处理 +- 不支持对受保护文档进行处理 + +说明: + +- 只读文档应支持处理。 +- 对于不支持或无法打开的文档,系统应记录错误并跳过当前文件,继续处理后续文件。 + +## 6. 替换功能需求 + +### 6.1 功能描述 + +对原始文件 `[A]` 执行替换操作,输出: + +- 替换后文件 `[B]` +- 差异文件 `[C]` 的 JSON 形式 +- 差异文件 `[C]` 的 BMP 形式 + +### 6.2 处理模式 + +替换处理支持目录批处理: + +- 以用户选择的原始文件夹为输入根目录 +- 自动扫描该目录及其所有子目录下的 `*.doc` 和 `*.docx` 文件 +- 用户可对扫描结果进行勾选控制 +- 仅处理当前勾选的文件 + +### 6.3 输出目录与目录结构 + +替换输出应满足: + +- 以用户选择的替换文件夹为输出根目录 +- 相对于原始文件夹的目录树结构必须被完整保留 +- 替换后文件与差异文件保存在替换文件夹下对应相对路径目录中 + +### 6.4 命名规则 + +替换模式下输出文件命名规则如下: + +- 原始文件的文件基本名(不含目录与扩展名)必须纳入启用规则的匹配和替换范围 +- 文件名匹配使用与正文文字一致的规则顺序、普通文本/正则模式、大小写开关、全字匹配开关和替换模板语义 +- 目录路径与扩展名不参与文件名规则匹配和替换 +- 若文件名未命中任何规则,则使用原始文件基本名 +- 替换后文件名:`规则替换后的文件基本名-yyyyMMdd.原始后缀` +- 差异文件 JSON 名:`规则替换后的文件基本名-yyyyMMdd.diff` +- 差异文件 BMP 名:`规则替换后的文件基本名-yyyyMMdd.bmp` +- 差异文件必须在文档元数据中记录原始文件名与实际替换后文件名,用于恢复时还原文件名 +- 当原始文件名与实际替换后文件名不一致时,差异文件的修改记录中必须记录一条文件名变更操作,用于审计文件名变化 +- 若规则替换后的文件基本名为空或包含非法文件名字符,应阻断当前扫描/执行并在日志中记录源文件、规则结果和错误原因 + +若出现同名冲突: + +- 不覆盖现有文件 +- 自动在文件名后追加自增序号 +- 同一源文件产生的替换文档、`*.diff`、`*.bmp` 中任一输出发生同名冲突时,三者必须使用相同的自增序号,确保恢复扫描可按同一文件基本名配对 + +示例: + +- 原始文件 `示例.docx`,文件名规则将 `示例` 替换为 `样例` +- `样例-20260424.docx` +- `样例-20260424_1.docx` +- `样例-20260424.diff` +- `样例-20260424_1.diff` +- `样例-20260424.bmp` +- `样例-20260424_1.bmp` + +## 7. 文字替换规则 + +### 7.1 基本规则 + +每条规则用于将文字片段 `alpha` 替换为 `beta`。 + +规则按列表顺序依次执行。 + +前一条规则替换后的结果允许被后一条规则再次命中。 + +当多条规则命中同一位置时: + +- 以规则顺序优先 + +### 7.2 规则文件格式 + +规则文件采用 JSON 格式保存: + +- 文件后缀:`*.rule` + +### 7.3 每条规则字段 + +每条规则至少包含以下字段: + +- 规则 ID +- 规则名称 +- 是否启用 +- 匹配模式 +- 是否区分大小写 +- 是否全字匹配 +- 原文本或正则表达式 +- 新文本或替换模板 +- 备注 + +字段说明: + +- 规则 ID 必须唯一 +- 匹配模式取值为: + - 普通文本 + - 正则表达式 +- 区分大小写默认值为:否 +- 全字匹配默认值为:否 + +### 7.4 普通模式与正则模式 + +文字替换应支持: + +- 普通文本模式 +- 正则表达式模式 + +正则模式要求: + +- 支持捕获组 +- 支持捕获组回填 + +### 7.5 匹配边界要求 + +匹配应满足: + +- 允许跨 Run 匹配 +- 不允许跨段落匹配 +- 不允许跨单元格匹配 + +说明: + +- 在同一段落、同一文本框、同一单元格内部,视觉连续但被拆分为多个 Run 的文本,应允许被视为同一次匹配。 + +### 7.6 样式保持要求 + +文字替换后必须保持以下样式不变: + +- 字体 +- 字号 +- 颜色 +- 粗体、斜体、下划线等字符样式 +- 缩进 +- 行间距 +- 段前距 +- 段后距 +- 编号格式 + +编号相关要求: + +- 编号值不变 +- 编号级别不变 +- 缩进不变 +- 制表位不变 +- 自动编号行为不变 + +### 7.7 占位要求 + +文字替换后不得改变原有版式。 + +“占位”定义为: + +- 屏幕显示字符宽度 + +若新文本显示宽度大于原文本: + +- 应对新文本进行截断 + +若新文本显示宽度小于原文本: + +- 允许采用适当空白填充或等效方式补齐 + +补齐规则要求: + +- 只要能够保持原版式不变且支持完整恢复,具体空白实现方式不限 +- 若“补齐宽度”和“版式稳定”冲突,以版式稳定为优先 + +### 7.8 可恢复性记录要求 + +每一处文字替换都必须写入差异文件,至少记录: + +- 替换类型,标记为“文本替换” +- 规则 ID +- 规则名称 +- 规则表达式或原文本 +- 当前规则命中序号 +- 位置起点 +- 位置终点 +- 被替换前文本 +- 替换后文本 +- 所属对象类型 + +### 7.9 自动提取生成规则要求 + +系统应支持从当前启用的文档集合自动提取候选规则。 + +该功能要求如下: + +- 入口位于替换页面 +- 提取对象为替换页面文件列表中复选框已勾选的全部文档 +- 当文件列表未勾选任何文件时,自动提取不得执行 +- 文件列表中的高亮选中状态不影响自动提取扫描范围 +- 提取范围为全部勾选文档中纳入文字替换范围的可见文本内容 +- 提取范围至少包括: + - 正文 + - 页眉页脚 + - 表格 + - 文本框 + - 批注 + - 超链接显示文字 + - 域结果 + - 公式文字 + - 图表文字 +- 仅提取可见文本 +- 不提取隐藏文字、域代码、修订删除内容和对象内部不可见元数据 +- 提取内容至少包含: + - 关键字 + - 高频词 + - 高频句 +- 提取类别应在设置页面中可配置,默认全选 +- 自动提取不设置扫描页数或字符数上限,应对全部勾选文档全文分析 +- 自动提取功能优先保证 `*.docx`,对 `*.doc` 以实际可解析文本范围为准 + +提取算法与筛选要求: + +- 关键字提取应支持以下算法,并允许多选: + - 按词频 + - `TF-IDF` + - `TextRank` +- 关键字提取算法默认全选 +- 执行自动提取时,应对勾选文件范围按所选关键字算法各执行一轮,并对结果去重合并后进入预览 +- 高频词提取应支持中文词项和英文单词两个统计选项,默认全选 +- 高频句提取应支持中文句和英文句两个统计选项,默认全选 +- 高频句的句子分隔符应在设置页面中可配置,默认启用常见中英文句末分隔符 +- 单字和单词长度限制应在设置页面中可配置 +- 中文词项最小长度默认值建议为 `1` +- 英文单词最小长度默认值建议为 `1` +- 邮箱地址默认参与提取 +- 其他内容如纯数字、日期、编号序列、页码、URL 不默认排除 + +提取结果处理要求: + +- 提取完成后,必须先展示提取结果预览,而不是直接追加 +- 预览界面中,用户应可按条选择是否追加 +- 预览结果应至少显示: + - 是否追加 + - 提取类型 + - 原文本 + - 统计值 + - 首次出现文档路径 + - 首次出现顺序 +- 提取结果应按首次出现顺序排序 +- 同一原文本在同一次提取结果中去重后仅保留一次 +- 用户确认后,每一条被选中的提取结果应追加为一条新规则 +- 追加目标为当前会话内存中的规则列表 +- 追加后应立即反映到设置页面的规则列表中 +- 规则追加后,只有在用户执行“保存规则文件”后,才写入 `*.rule` 文件 + +自动生成规则的默认字段要求: + +- 规则 ID:系统自动生成,且必须唯一 +- 自动提取生成的规则 ID 应按提取类型使用前缀: + - 关键字:`AK` + - 高频词:`AW` + - 高频句:`AS` +- 规则名称:系统自动生成 +- 自动提取生成的规则名称建议格式为: + - 关键字:`AUTO-KW-0001` + - 高频词:`AUTO-WORD-0001` + - 高频句:`AUTO-SENT-0001` +- 是否启用:是 +- 匹配模式:普通文本 +- 是否区分大小写:是 +- 是否全字匹配:是 +- 原文本或表达式:提取结果原文 +- 新文本或替换模板:默认初始化为空字符串 +- 备注:应标记为自动提取,并至少记录提取类型、统计值、首次出现文档路径和提取时间 + +补充要求: + +- 自动提取规则的“规则生效”定义为规则启用状态为“是” +- 即使新文本或替换模板为空字符串,自动提取生成的规则仍必须默认处于启用状态 +- 系统不得因自动提取规则的新文本为空而自动禁用、自动填充或阻止用户执行后续替换流程 +- 对于原文本为空、纯空白或无效的提取结果,不得追加 +- 对于与当前规则列表中原文本相同的普通文本规则,应视为重复项并避免追加 +- 若现有人工规则与自动提取结果原文本相同,则必须保留人工规则并丢弃自动提取结果 +- 即使现有规则的大小写选项或新文本不同,只要原文本相同,自动提取结果仍视为重复项 +- 在用户确认追加前,应执行快速去重和冲突检查 +- 在用户确认追加后,应对完整规则列表执行正式合法性和兼容性检查 +- 提取过程中必须显示进度,并允许用户取消 +- 提取过程中不允许用户编辑规则列表 +- 用户取消提取后,不得修改当前规则列表 +- 追加完成后,系统应自动切换到设置页面并定位到第一条新增规则 +- 系统必须提供“撤销本次自动提取追加结果”的操作 +- 撤销应仅回退最近一次自动提取成功追加的规则集合 +- 若未提取到任何有效结果,必须弹窗提示“未提取到可用项” +- 提取完成后,系统应通过弹窗和日志给出提取总数、成功追加数、跳过数 +- 自动提取失败时,不得破坏现有规则列表,并必须写入日志 + +## 8. 图片替换规则 + +### 8.1 位图图片范围 + +位图图片至少支持以下格式: + +- BMP +- PNG +- JPG / JPEG +- TIFF +- GIF + +### 8.2 非位图图片范围 + +非位图图片或对象至少支持以下类型: + +- WMF +- EMF +- SVG +- Office 形状对象 +- 图表对象 + +### 8.3 位图图片替换方式 + +对位图图片执行如下处理: + +- 在原图基础上添加随机噪声形成新图 +- 噪声类型包含: + - 点噪声 + - 线噪声 + - 块噪声 +- 噪声应基本均匀分布于全图范围 +- 被修改的像素数占比不得低于原图像素总数的 50% + +### 8.4 非位图图片替换方式 + +对非位图对象执行如下处理: + +- 先转换为位图 +- 转换后保持原显示占位不变 +- 再按位图图片替换方式处理 + +### 8.5 图片属性保持要求 + +图片替换后必须保持以下属性不变: + +- 显示尺寸 +- 页面位置 +- 环绕方式 +- 锚点位置 +- 裁剪参数 +- 旋转角度 + +### 8.6 随机性要求 + +图片替换要求如下: + +- 默认每次执行时噪声应随机不同 +- 系统应支持固定随机种子,以便相同输入可重复生成相同结果 +- 噪声类型比例不提供用户配置项,由系统自动设置最优方案 + +### 8.7 非位图恢复要求 + +非位图对象在恢复时: + +- 只恢复为位图形式 +- 不要求恢复为原始非位图对象类型 + +### 8.8 图片差异记录要求 + +每一处图片替换都必须写入差异文件,至少记录: + +- 替换类型,标记为“图像替换” +- 对象位置 +- 原图数据 +- 若原图为非位图,则记录转换后的位图数据 +- 替换后新图数据 +- 随机种子 +- 原图尺寸 +- 替换后尺寸 +- 所属对象类型 + +## 9. 其他对象处理规则 + +### 9.1 超链接 + +超链接仅处理: + +- 显示文字 + +不处理: + +- 目标 URL + +### 9.2 目录 + +目录处理要求: + +- 替换时仅处理目录当前显示结果 +- 替换完成后必须自动更新目录 +- 恢复完成后必须自动更新目录 + +### 9.3 域 + +域处理要求: + +- 仅处理域显示结果 +- 不处理域代码 +- 替换完成和恢复完成后必须刷新域,并验证文档显示结果中不得存在“错误!未找到引用源”等交叉引用或域结果错误 +- 若验证发现交叉引用或域结果错误,应拒绝输出当前文件,并在日志中记录足够定位的 Story、容器、段落和上下文片段 + +### 9.4 公式 + +公式处理要求: + +- 解析公式中的可解析文字内容进行替换 +- 不将公式整体按图像处理 + +### 9.5 图表 + +图表中以下文本内容均在处理范围内: + +- 标题 +- 坐标轴标题 +- 图例 +- 数据标签 +- 图表内嵌数据表文本 + +### 9.6 批注 + +批注处理要求: + +- 仅修改批注内容 +- 必须保留批注作者 +- 必须保留批注时间 +- 必须保留批注状态 + +## 10. 还原功能需求 + +### 10.1 功能描述 + +通过读取替换后文件 `[B]` 与差异文件 `[C]`,生成还原文件 `[A']`。 + +### 10.2 还原匹配原则 + +还原过程采用严格匹配,不允许容错查找。 + +即: + +- 必须按差异文件中记录的位置严格定位 +- 必须按差异文件中记录的当前内容进行严格比对 +- 任一关键匹配条件不成立时,该处恢复失败 + +### 10.3 位置模型 + +差异文件中的位置定义采用以下模型: + +- Story 类型 +- 容器路径 +- 段落索引 +- 起始 Run 索引 +- 起始字符偏移 +- 结束 Run 索引 +- 结束字符偏移 + +说明: + +- 文本对象使用上述位置模型定位 +- 非文本对象应在上述逻辑容器基础上补充对象索引定位 + +### 10.4 恢复输入格式 + +恢复模式下,用户一次只能选择一种差异文件输入格式: + +- `*.diff` +- `*.bmp` + +若用户选择 `*.bmp`: + +- 程序应先自动解码得到与 JSON 差异数据逻辑等价的数据 +- 然后按统一恢复逻辑执行恢复 + +程序不应在同一次批处理中自动混用两种差异文件格式。 + +### 10.5 恢复输出目录与命名 + +恢复模式输出要求: + +- 以用户选择的恢复文件夹为输出根目录 +- 相对于替换文件夹的目录树结构必须完整保留 +- 恢复时必须优先读取差异文件中的 `sourceDocument.fileName` + +恢复文件命名规则如下: + +- 对于包含文件名元数据的新差异文件:恢复输出文件名应还原为原始文件名与原始扩展名 +- 示例:`示例.docx` 经文件名规则替换输出为 `样例-20260424.docx`,恢复输出应为 `示例.docx` +- 对于缺少原始文件名元数据的旧差异文件,允许使用兼容命名:`替换后文件名-rcv.替换后后缀` + +若发生同名冲突: + +- 不覆盖 +- 自动追加自增序号 + +### 10.6 异常处理 + +还原过程中: + +- 单个文件失败不应导致整个批处理终止 +- 系统应记录错误并继续处理后续文件 +- 处理结束后必须给出汇总结果 + +## 11. 可逆性与一致性要求 + +### 11.1 文字一致性 + +恢复后的 `[A']` 必须在文字内容上与 `[A]` 完全一致。 + +文字内容一致的验收口径为: + +- 按纯文本完全一致验收 + +### 11.2 排版一致性 + +恢复后的 `[A']` 必须在排版上与 `[A]` 一致。 + +排版一致的验收至少包括: + +- 页数一致 +- 分页位置一致 +- 段落分页一致 +- 表格分页一致 +- 行数一致 + +### 11.3 插图一致性 + +恢复后的插图必须与原文件在视觉上保持一致。 + +插图一致性验收至少同时包括: + +- 肉眼比对一致 +- 尺寸和位置一致 +- 像素相似度达到测试规范定义阈值 + +说明: + +- 像素相似度阈值由测试规范另行定义。 + +## 12. 规则兼容性检查 + +### 12.1 检查时机 + +规则兼容性检查必须在以下时机执行: + +- 保存规则文件时 +- 执行替换前 + +### 12.2 检查内容 + +兼容性检查至少覆盖: + +- 正则表达式非法 +- 规则自身替换后再次命中自身 +- 多条规则命中同一区域 +- 规则间循环影响 +- 规则前后覆盖关系 +- 明显不可恢复风险 + +### 12.3 检查结果处理 + +当发现正则语法错误等致命错误时: + +- 禁止保存规则文件 +- 通过日志提示错误 +- 日志必须能够定位规则位置 +- 日志必须提供详细错误信息 + +当发现兼容性风险但不属于致命错误时: + +- 允许用户继续执行 +- 必须弹窗提示 +- 必须写入日志 + +### 12.4 执行门槛 + +规则兼容性检查必须在执行前完成。 + +对于非致命风险: + +- 允许用户确认后继续执行 + +## 13. 差异文件要求 + +### 13.1 总体要求 + +差异文件必须同时生成两种形式: + +- JSON 形式 +- BMP 形式 + +两种形式必须双向无损转换。 + +### 13.2 JSON 差异文件 + +JSON 差异文件要求如下: + +- 文件后缀:`*.diff` +- 图片差异数据必须内嵌保存 +- 不允许依赖外部图片文件进行恢复 +- 差异文件必须包含完整恢复所需信息 + +### 13.3 JSON 加密选项 + +系统必须提供 JSON 差异文件加密选项: + +- 可选值:加密 / 不加密 +- 默认值:加密 + +该选项要求: + +- 记录到会话配置文件中 +- 影响后续生成的 `*.diff` 文件 + +说明: + +- 由于系统同时必须生成不加密的 `*.bmp` 差异文件,`*.diff` 的加密能力仅适用于该文件自身的存储形式 +- 当 `*.bmp` 同时存在时,不应将 `*.diff` 加密视为整体差异数据的保密性保证 + +### 13.4 BMP 差异文件 + +BMP 差异文件要求如下: + +- 文件后缀:`*.bmp` +- 与对应 `*.diff` 文件同名同目录生成 +- 必须能够无损还原出逻辑等价的差异数据 +- BMP 文件本身不加密 + +### 13.5 校验与安全性 + +差异文件必须具备: + +- 完整性校验 +- 防篡改能力 + +差异文件至少应记录以下校验与识别信息: + +- 原始文件指纹 +- 替换后文件指纹 +- 规则文件指纹 +- 程序版本 +- 差异算法版本 + +若差异文件校验失败: + +- 必须拒绝恢复 +- 不允许强制继续 + +若恢复时发现替换后文件 `[B]` 与差异文件 `[C]` 不匹配: + +- 必须拒绝恢复 +- 记录明确错误原因 +- 不允许强制继续 + +## 14. 人机界面要求 + +### 14.1 通用界面要求 + +系统界面固定为以下三个 Tab 页: + +- 替换 +- 恢复 +- 设置 + +批处理期间界面要求: + +- 不得卡死 +- 状态必须持续刷新 +- 日志必须持续刷新 +- 进度条必须持续刷新 + +### 14.2 目录选择对话框要求 + +凡涉及文件夹路径选择,均应满足: + +- 单击按钮后,弹出系统现代风格的目录选择对话框 +- 对话框界面风格应与文件打开对话框一致,但选择目标为文件夹 +- 不使用传统树形“浏览文件夹”对话框 +- 用户点击“确定”后,关闭对话框,并将所选文件夹绝对路径填入对应文本框 +- 用户点击“取消”后,关闭对话框,不更新对应文本框内容 + +### 14.3 替换页面 + +替换页面包含以下主要控件: + +- 选择原始文件夹按钮 +- 原始文件夹路径文本框 +- 选择替换文件夹按钮 +- 替换文件夹路径文本框 +- 文件列表 +- 自动提取按钮 +- 开始替换按钮 +- 停止按钮 +- 总进度条 +- 单文件进度条 + +交互要求: + +- 当原始文件夹路径文本框或替换文件夹路径文本框失去输入焦点后,应自动异步扫描原始文件夹 +- 自动加载目标目录及子目录下符合后缀的文件 +- 扫描过程中界面不得无响应 +- 文件列表默认全部勾选 +- 支持单选和多选 +- 支持批量勾选和批量取消勾选 +- 支持右键批量操作 +- 右键菜单至少应提供:删除、启用、不启用 +- 右键菜单批量操作应作用于当前选中的一行或多行 +- 支持按列排序 +- 自动提取按钮用于对当前勾选启用的文件集合执行关键字、高频词、高频句提取 +- 当文件列表未勾选任何文件时,自动提取按钮应禁用或给出明确提示 +- 当文件列表勾选了一个或多个文件时,点击自动提取按钮后,应对全部勾选文档执行提取 +- 文件列表的单选或多选状态仅用于界面交互,不影响自动提取的扫描范围 +- 点击自动提取后,应先进入提取结果预览流程 +- 自动提取过程中必须显示处理进度,并允许用户取消 +- 自动提取过程中界面不得假死 +- 自动提取过程中不允许编辑规则列表 +- 预览界面中,用户应可选择性勾选要追加的候选项 +- 用户确认追加后,提取结果应追加到当前规则列表,并立即反映到设置页面的规则列表中 +- 追加完成后,应自动切换到设置页面并定位到第一条新增规则 +- 系统应提供“撤销本次自动提取追加结果”操作 +- 提取完成后,必须弹出结果摘要,并将提取数量、追加数量、跳过数量写入日志 +- 若未提取到任何有效结果,必须弹窗提示“未提取到可用项” + +文件列表至少包含以下列: + +- 原始文件路径 +- 替换后文件路径 +- 差异文件路径 +- 文本替换次数 +- 图片替换次数 +- 当前状态 + +停止按钮语义: + +- 用户点击停止后,系统应尽量完成当前正在处理的文件 +- 当前文件结束后再停止后续任务 + +状态显示至少包含: + +- 未处理 +- 处理中 +- 成功 +- 失败 +- 已停止 + +颜色要求: + +- 成功:绿色 +- 失败:红色 +- 处理中:蓝色 +- 已停止:灰色 + +运行过程中必须显示: + +- 当前正在处理的文件编号 / 总文件数 +- 当前规则编号与规则名称 +- 当前文件命中次数 +- 累计命中次数 + +执行完成后: + +- 必须弹出汇总结果对话框 + +### 14.4 恢复页面 + +恢复页面包含以下主要控件: + +- 选择替换文件夹按钮 +- 替换文件夹路径文本框 +- 选择恢复文件夹按钮 +- 恢复文件夹路径文本框 +- 文件列表 +- 开始恢复按钮 +- 停止按钮 +- 总进度条 +- 单文件进度条 + +交互要求: + +- 当相关路径文本框失去输入焦点后,应自动异步扫描 +- 自动加载目标目录及子目录下可恢复的文件 +- 单次批处理仅允许使用一种差异文件输入格式 +- 支持右键批量操作 +- 右键菜单至少应提供:删除、启用、不启用 +- 右键菜单批量操作应作用于当前选中的一行或多行 + +文件列表至少包含以下列: + +- 替换后文件路径 +- 差异文件路径 +- 恢复文件路径 +- 文本恢复次数 +- 图片恢复次数 +- 当前状态 + +停止按钮语义与替换页面一致。 + +执行完成后: + +- 必须弹出汇总结果对话框 + +### 14.5 设置页面 + +设置页面包含以下主要控件: + +- 打开规则文件按钮 +- 规则文件路径显示区 +- 保存规则文件按钮 +- 规则列表 +- 自动提取设置区 +- 规则新增按钮 +- 规则删除按钮 +- 规则上移按钮 +- 规则下移按钮 + +规则列表至少包含以下字段列: + +- 规则 ID +- 规则名称 +- 是否启用(复选框) +- 匹配模式 +- 是否区分大小写 +- 是否全字匹配 +- 原文本或表达式 +- 新文本或替换模板 +- 备注 + +设置页面要求: + +- 仅保留“打开”和“保存”操作 +- 不提供导入、导出、另存为等独立功能 +- 规则列表支持单选和多选 +- 规则列表支持右键批量操作 +- 规则列表右键菜单至少应提供:删除、启用、不启用 +- 规则列表右键批量操作应作用于当前选中的一行或多行 +- 保存时必须执行规则合法性和兼容性检查 +- 检查结果必须同时输出到弹窗和日志 +- 替换页面自动提取追加的规则,必须立即显示在规则列表中 +- 自动提取设置区至少应提供以下配置项: + - 提取类别选择:关键字、高频词、高频句,默认全选 + - 关键字提取算法选择:按词频、`TF-IDF`、`TextRank`,支持多选,默认全选 + - 关键字最大提取数量,默认 `10` + - 高频词最大提取数量,默认 `10` + - 高频句最大提取数量,默认 `10` + - 高频词统计选项:中文词项、英文单词,默认全选 + - 高频句统计选项:中文句、英文句,默认全选 + - 中文词项最小长度 + - 英文单词最小长度 + - 句子分隔符选项 + - 是否排除邮箱地址,默认否 +- 自动提取相关设置属于会话配置,重启后应从 `config.json` 恢复 + +## 15. 日志要求 + +### 15.1 日志显示与存储 + +系统必须同时提供: + +- 界面日志框显示 +- 日志文件落盘 + +界面日志框要求: + +- 支持复制日志内容 +- 支持右键菜单 +- 右键菜单至少应提供:复制全部内容、清空日志 +- “复制全部内容”应复制当前日志框中的全部文本内容 +- “清空日志”仅清空界面日志框显示内容,不影响已落盘日志文件 + +日志编码固定为: + +- UTF-8 + +日志文件要求: + +- 保存于启动目录 +- 采用按天追加写入方式 +- 启动目录必须可写;若不可写,则视为运行环境不满足要求 + +日志文件命名规则: + +- `session-yyyyMMdd.log` + +### 15.2 日志级别与颜色 + +日志级别至少包括: + +- INFO +- WARNING +- ERROR + +显示颜色要求: + +- INFO:绿色 +- WARNING:橙色 +- ERROR:红色 +- 一般过程信息:黑色 + +### 15.3 日志内容 + +日志至少记录: + +- 文件操作记录 +- 当前处理源文件 +- 生成的替换文件路径 +- 生成的 JSON 差异文件路径 +- 生成的 BMP 差异文件路径 +- 当前处理的规则编号与规则名称 +- 匹配成功的原文本 +- 匹配成功的起始位置和结束位置 +- 当前文件序号与总文件数 +- 当前规则序号与总规则数 +- 当前规则命中序号 +- 恢复记录 +- 恢复失败原因 +- 异常堆栈 +- 规则兼容性检查结果及严重级别 +- 图片替换时的随机种子 +- 图片替换时的尺寸信息 +- 批处理汇总信息 + +### 15.4 批处理汇总 + +批处理汇总日志至少包含: + +- 成功文件数 +- 失败文件数 +- 跳过文件数 +- 文本替换总次数 +- 图片替换总次数 +- 文本恢复总次数 +- 图片恢复总次数 + +## 16. 会话配置要求 + +### 16.1 基本要求 + +系统应以 JSON 格式保存当前会话信息: + +- 文件名:`config.json` +- 保存目录:启动目录 + +会话信息变化后应自动刷新配置文件。 + +启动时若存在 `config.json`,应自动加载。 + +### 16.2 配置项 + +会话配置至少包含: + +- 当前规则文件路径 +- 替换页面的原始文件夹路径 +- 替换页面的替换文件夹路径 +- 恢复页面的替换文件夹路径 +- 恢复页面的恢复文件夹路径 +- JSON 差异文件加密开关 + +## 17. 兼容性与运行环境要求 + +### 17.1 操作系统 + +系统必须兼容以下离线原生纯净系统: + +- Windows 7 SP1 x64 +- Windows 10 x64 +- Windows 11 x64 +- Windows Server 2022 x64 + +### 17.2 运行架构 + +- 仅支持 x64 +- 程序应以普通用户权限运行 +- 不依赖管理员权限 +- 目标机不具有管理员权限 +- 目标机不具有软件安装权限 + +### 17.3 依赖要求 + +- 所有依赖必须随程序离线打包 +- 目标系统不应要求预装第三方运行时或工具;若 .NET Framework 4.8 无法真正自包含,应在交付风险中明确说明 +- 允许程序首次启动时将内置组件解压到本地临时目录 +- 替换和恢复宿主机必须已安装支持 `*.docx` 的 Word 或 WPS +- 不得要求安装 Visio;Visio/OLE 等对象按可见位图表示处理 +- 第三方库应选择免费授权的非商业第三方库,且可满足项目使用要求 + +### 17.4 交付形式 + +交付形式要求如下: + +- 仅提供绿色版 +- 不提供安装版 +- 不要求桌面快捷方式、开始菜单或卸载项 + +若采用覆盖升级方式分发新版本,应保留: + +- `config.json` +- 日志文件 +- `*.rule` 规则文件 + +## 18. 性能与响应要求 + +### 18.1 目标性能指标 + +以下指标为单个文件处理的目标性能,不作为强制验收门槛: + +- 替换处理 2000 页 Word 文件不超过 180 秒 +- 还原处理 2000 页 Word 文件不超过 180 秒 +- 替换处理 100 页 Word 文件不超过 10 秒 +- 还原处理 100 页 Word 文件不超过 10 秒 + +说明: + +- 目录扫描时间不计入上述指标 +- 日志写入时间不计入上述指标 +- BMP 差异文件生成时间不计入上述指标 + +### 18.2 大文件与多文件响应性 + +即使在大文件或多文件场景下,系统也必须满足: + +- 不出现界面卡死 +- 不出现状态不更新 +- 不出现操作无响应 +- 支持异步扫描 +- 支持并行处理多个文件 + +并行处理要求: + +- 并发度不提供用户配置项 +- 由系统根据运行环境自动尽可能利用可用资源 + +## 19. 标准验收要求 + +### 19.1 标准验收样例集 + +项目必须提供标准验收样例集,至少覆盖: + +- 页眉 +- 页脚 +- 文本框 +- 表格 +- 合并单元格 +- 批注 +- 目录 +- 域 +- 公式 +- 图表 +- 位图图片 +- 非位图图片 +- `*.doc` +- `*.docx` + +### 19.2 验收输出物 + +项目应提供: + +- 测试报告 +- 验收报告模板 + +### 19.3 执行前门槛 + +执行替换前至少满足: + +- 规则文件加载成功 +- 规则合法性检查通过 +- 规则兼容性检查已完成 +- 输入路径有效 +- 输出路径有效且可写 + +## 20. 异常防护与故障处理要求 + +### 20.1 总体原则 + +系统应满足以下总体故障处理原则: + +- 单文件失败不影响后续文件继续处理 +- 文件级错误必须在日志中完整记录 +- 汇总结果必须明确显示成功、失败、跳过数量 +- 停止操作应在当前文件尽量处理完成后生效 +- 以“可恢复性”和“一致性”为最高优先级,不得为了继续执行而牺牲可逆性 + +### 20.2 路径与目录防护 + +执行前必须完成路径合法性检查。 + +以下情况应视为非法配置并阻止执行: + +- 原始文件夹与替换文件夹相同 +- 原始文件夹与恢复文件夹相同 +- 替换文件夹与恢复文件夹相同 +- 任意两个上述目录互为父子目录 +- 输出目标路径与输入源路径重合 + +执行过程中还必须处理以下路径异常: + +- 路径过长 +- 文件名或路径包含非法字符 +- 输出目录不存在 +- 输出目录只读 +- 输出目录无写权限 + +对于批处理根路径非法: + +- 整个批处理不得启动 + +对于单文件目标路径异常: + +- 当前文件失败 +- 记录日志 +- 继续处理后续文件 + +### 20.3 原子写入与半成品防护 + +每个文件的替换与恢复都必须采用原子提交策略。 + +执行要求如下: + +- 正式输出文件 `[B]`、`*.diff`、`*.bmp`、`[A']` 必须先写入临时文件 +- 仅当当前文件的全部必需输出均成功生成且校验通过后,才允许提交为正式文件 +- 若其中任一输出生成失败、校验失败或写入失败,则该文件整体视为失败 + +失败时必须满足: + +- 不保留正式半成品 +- 尽可能清理当前文件产生的临时文件 +- 日志中必须标明清理结果 + +### 20.4 文件占用与外部变更防护 + +系统必须检测并处理以下场景: + +- 原始文件被其他进程占用 +- 替换后文件目标被占用 +- 差异文件目标被占用 +- 恢复文件目标被占用 +- 文件在扫描后至处理前被删除、移动或替换 +- 文件在处理过程中被外部修改 + +处理要求如下: + +- 当前文件失败 +- 记录明确错误原因 +- 继续处理后续文件 + +### 20.5 资源与环境异常防护 + +系统必须防护以下异常: + +- 磁盘空间不足 +- 临时目录不可写 +- 启动目录不可写 +- 内存不足 +- 图片渲染失败 +- 非位图转位图失败 +- 并发过高导致系统无响应 + +处理要求如下: + +- 启动目录不可写时,视为运行环境不满足要求,阻止执行 +- 临时目录不可写时,视为当前批次运行环境不满足要求,阻止当前批次执行 +- 单文件处理中出现资源类异常时,当前文件失败并继续后续文件 +- 系统应在并发处理时自动控制资源使用,优先保证界面可响应 + +### 20.6 并发与重复处理防护 + +并发处理时必须满足: + +- 同一源文件不得重复入队 +- 同一输出目标路径不得被多个任务同时占用 +- 扫描结果必须去重 +- 同一差异文件目标不得被多个任务同时生成 +- 同一恢复输出目标不得被多个任务同时生成 + +系统应采用互斥或等效机制保证上述约束。 + +### 20.7 规则执行异常防护 + +除规则兼容性检查外,执行期还必须防护以下问题: + +- 零长度匹配导致死循环 +- 正则灾难性回溯导致长时间卡死 +- 非法捕获组回填引用 +- 单条规则在单文件中的异常高频命中 +- 规则链式替换导致处理次数异常膨胀 + +处理要求如下: + +- 系统应设置内部安全阈值和超时保护 +- 触发保护时,当前文件失败 +- 日志中必须记录触发的规则、原因和触发位置 + +### 20.8 对象级异常处理策略 + +以下对象在解析、替换或恢复过程中若任一对象处理失败: + +- 文本框 +- 形状 +- 图表 +- 公式 +- 表格 +- 图片 +- 批注 +- 页眉页脚中的对象 + +则默认处理策略为: + +- 跳过当前对象 +- 继续处理当前文件中的后续对象 +- 日志中必须记录对象类型、Story、容器路径、位置、操作 ID 和失败原因 +- 文件级全局校验、打开、写入、完整性或指纹错误仍按当前文件失败处理 + +说明: + +- 该策略用于在不破坏当前文件整体处理的前提下保留可诊断信息;不得静默隐藏局部失败 + +### 20.9 字体环境异常防护 + +系统必须在执行前进行字体环境检查。 + +检查目标包括: + +- 当前处理文件中使用的字体是否在目标机可用 +- 缺失字体是否可能影响排版一致性 + +由于目标环境可能离线且不具备字体安装权限,当发现缺失字体时: + +- 不得尝试联网下载、更新或安装字体 +- 当前文件继续执行替换或恢复 +- 中文及东亚文字默认使用 `宋体` 兜底 +- 英文、数字及拉丁文字默认使用 `Times New Roman` 兜底 +- 日志中必须记录缺失字体清单、兜底字体和影响说明 + +### 20.10 差异文件错配与恢复防护 + +恢复前必须校验以下信息: + +- 替换后文件 `[B]` 与差异文件 `[C]` 的指纹对应关系 +- 差异文件的完整性校验值 +- 差异文件的防篡改校验结果 +- 差异文件所对应的规则文件指纹 +- 差异文件所对应的程序版本和算法版本 + +当出现以下任一情况时: + +- 指纹不匹配 +- 校验失败 +- 防篡改校验失败 +- 关键版本信息不兼容 + +则: + +- 必须拒绝恢复 +- 不允许强制继续 +- 日志中必须记录明确原因 + +### 20.11 停止、崩溃与中断恢复 + +用户点击停止时: + +- 系统应尽量完成当前文件 +- 当前文件结束后停止调度新的文件 +- 尚未开始的文件标记为“已停止” + +程序异常退出、系统崩溃或断电后: + +- 下次启动时应自动检测上次遗留的临时文件 +- 对可安全清理的临时文件自动清理 +- 清理结果写入日志 + +对于无法安全判断是否可清理的遗留文件: + +- 不得自动作为正式输出使用 +- 必须提示用户并记录日志 + +### 20.12 配置与规则文件损坏防护 + +当 `config.json` 缺失时: + +- 系统可按默认会话配置启动 +- 首次保存配置时自动生成新的 `config.json` +- 写入日志 + +当 `config.json` 损坏或无法解析时: + +- 系统必须提示用户配置文件已损坏 +- 在用户确认重建默认配置或修复原配置前,禁止启动替换、恢复和自动提取等执行型任务 +- 不得按损坏配置继续执行 +- 写入日志 + +当 `*.rule` 文件损坏、缺失或无法解析时: + +- 不允许启动替换任务 +- 必须提示用户重新加载或修复规则文件 +- 写入日志 + +### 20.13 日志异常防护 + +当批处理开始前日志文件无法创建或无法打开时: + +- 不允许启动批处理任务 +- 必须提示用户运行环境不满足要求 + +当批处理过程中日志文件追加写入失败时: + +- 系统应优先保证当前文件处理流程可控结束 +- 后续日志至少应继续保留在界面日志中 +- 当前批次在当前文件结束后不得继续调度新的文件 +- 必须提示用户日志落盘失败 + +## 21. 非功能性要求摘要 + +- 静默处理,不显示 Word 打开界面 +- UI 在长时间处理过程中保持可响应 +- 启动目录必须可写 +- 差异文件必须可校验 +- 差异文件 JSON 默认加密 +- BMP 差异文件不加密 +- 还原必须严格匹配,不允许模糊恢复 + +## 22. 附录 A:规则文件 `*.rule` JSON 结构 + +### 22.1 顶层结构 + +规则文件采用 JSON 对象作为顶层结构,建议如下: + +```json +{ + "format": "dcit-rule", + "version": "1.0", + "savedAt": "2026-04-24T20:15:30+08:00", + "generator": { + "appName": "DCIT", + "appVersion": "1.0.0" + }, + "rules": [] +} +``` + +顶层字段定义如下: + +- `format` + - 类型:`string` + - 必填:是 + - 固定值:`dcit-rule` +- `version` + - 类型:`string` + - 必填:是 + - 含义:规则文件结构版本 +- `savedAt` + - 类型:`string` + - 必填:是 + - 含义:规则文件保存时间,采用 ISO 8601 格式 +- `generator` + - 类型:`object` + - 必填:否 + - 含义:生成该规则文件的程序信息 +- `rules` + - 类型:`array` + - 必填:是 + - 含义:规则数组 + +### 22.2 规则数组执行语义 + +- `rules` 数组中的顺序即执行顺序 +- 保存时必须保持用户界面中的当前排序 +- 加载后必须按文件中的数组顺序恢复到界面 + +### 22.3 单条规则对象结构 + +每条规则对象结构如下: + +```json +{ + "id": "R001", + "name": "替换合同编号", + "enabled": true, + "matchMode": "plain", + "caseSensitive": false, + "wholeWord": false, + "pattern": "合同编号", + "replacement": "文件编号", + "note": "示例规则" +} +``` + +字段定义如下: + +- `id` + - 类型:`string` + - 必填:是 + - 要求:规则唯一标识,不可重复 +- `name` + - 类型:`string` + - 必填:是 + - 含义:规则名称 +- `enabled` + - 类型:`boolean` + - 必填:是 + - 含义:规则是否启用 +- `matchMode` + - 类型:`string` + - 必填:是 + - 可选值: + - `plain` + - `regex` +- `caseSensitive` + - 类型:`boolean` + - 必填:是 + - 默认值:`false` +- `wholeWord` + - 类型:`boolean` + - 必填:是 + - 默认值:`false` +- `pattern` + - 类型:`string` + - 必填:是 + - 含义:普通文本模式下的匹配文本,或正则模式下的表达式 +- `replacement` + - 类型:`string` + - 必填:是 + - 含义:替换文本或正则回填模板 +- `note` + - 类型:`string` + - 必填:否 + - 含义:备注 + +### 22.4 规则文件约束 + +- 不定义作用范围字段 +- 不定义图片替换参数字段 +- 图片替换策略采用全局固定策略 +- 当 `matchMode` 为 `regex` 时,`pattern` 必须通过正则合法性检查 +- 当正则语法非法时,不允许保存规则文件 + +### 22.5 规则文件示例 + +```json +{ + "format": "dcit-rule", + "version": "1.0", + "savedAt": "2026-04-24T20:15:30+08:00", + "generator": { + "appName": "DCIT", + "appVersion": "1.0.0" + }, + "rules": [ + { + "id": "R001", + "name": "替换合同编号", + "enabled": true, + "matchMode": "plain", + "caseSensitive": false, + "wholeWord": false, + "pattern": "合同编号", + "replacement": "文件编号", + "note": "普通文本规则" + }, + { + "id": "R002", + "name": "替换日期", + "enabled": true, + "matchMode": "regex", + "caseSensitive": false, + "wholeWord": false, + "pattern": "(20\\d{2})-(\\d{2})-(\\d{2})", + "replacement": "$1/$2/$3", + "note": "支持捕获组回填" + } + ] +} +``` + +## 23. 附录 B:差异文件 `*.diff` JSON 结构 + +### 23.1 总体结构 + +`*.diff` 文件采用 JSON 对象作为顶层结构。 + +当 JSON 差异文件不加密时,结构建议如下: + +```json +{ + "format": "dcit-diff", + "version": "1.0", + "createdAt": "2026-04-24T20:30:00+08:00", + "encrypted": false, + "app": {}, + "algorithm": {}, + "sourceDocument": {}, + "replacedDocument": {}, + "ruleFile": {}, + "integrity": {}, + "payload": {} +} +``` + +当 JSON 差异文件加密时,顶层结构建议如下: + +```json +{ + "format": "dcit-diff", + "version": "1.0", + "createdAt": "2026-04-24T20:30:00+08:00", + "encrypted": true, + "app": {}, + "algorithm": {}, + "sourceDocument": {}, + "replacedDocument": {}, + "ruleFile": {}, + "integrity": {}, + "encryption": {}, + "payloadCiphertext": "Base64..." +} +``` + +说明: + +- `payload` 表示明文差异数据载荷 +- `payloadCiphertext` 表示加密后的差异数据载荷 +- 两种模式下,`sourceDocument`、`replacedDocument`、`ruleFile`、`integrity` 的结构保持一致 +- `*.bmp` 中承载的是逻辑等价的明文差异数据,不承载 `payloadCiphertext` + +### 23.2 顶层公共字段 + +- `format` + - 类型:`string` + - 必填:是 + - 固定值:`dcit-diff` +- `version` + - 类型:`string` + - 必填:是 + - 含义:差异文件结构版本 +- `createdAt` + - 类型:`string` + - 必填:是 + - 含义:差异文件创建时间,采用 ISO 8601 格式 +- `encrypted` + - 类型:`boolean` + - 必填:是 + - 含义:当前 `*.diff` 是否加密存储 +- `app` + - 类型:`object` + - 必填:是 + - 含义:程序信息 +- `algorithm` + - 类型:`object` + - 必填:是 + - 含义:差异算法与 BMP 编码算法版本信息 +- `sourceDocument` + - 类型:`object` + - 必填:是 + - 含义:原始文件 `[A]` 的识别信息 +- `replacedDocument` + - 类型:`object` + - 必填:是 + - 含义:替换后文件 `[B]` 的识别信息 +- `ruleFile` + - 类型:`object` + - 必填:是 + - 含义:规则文件识别信息 +- `integrity` + - 类型:`object` + - 必填:是 + - 含义:完整性校验与防篡改信息 + +### 23.3 程序与算法信息结构 + +`app` 对象建议结构如下: + +```json +{ + "name": "DCIT", + "version": "1.0.0" +} +``` + +`algorithm` 对象建议结构如下: + +```json +{ + "diffVersion": "1.0", + "bmpCodecVersion": "1.0" +} +``` + +### 23.4 文档识别信息结构 + +`sourceDocument` 与 `replacedDocument` 对象建议结构如下: + +```json +{ + "fileName": "示例.docx", + "relativePath": "子目录/示例.docx", + "extension": ".docx", + "fingerprintAlgorithm": "SHA-256", + "fingerprint": "Base64..." +} +``` + +字段说明: + +- `fileName` + - 含义:文件名 + - `sourceDocument.fileName` 必须记录原始文件 `[A]` 的原始文件名 + - `replacedDocument.fileName` 必须记录实际生成的替换后文件 `[B]` 文件名,包括文件名规则替换和同名自增后的最终结果 +- `relativePath` + - 含义:相对根目录路径 +- `extension` + - 含义:原始扩展名 +- `fingerprintAlgorithm` + - 含义:文件指纹算法 +- `fingerprint` + - 含义:文件指纹值 + +`ruleFile` 对象建议结构如下: + +```json +{ + "fileName": "default.rule", + "version": "1.0", + "fingerprintAlgorithm": "SHA-256", + "fingerprint": "Base64..." +} +``` + +### 23.5 完整性与防篡改结构 + +`integrity` 对象建议结构如下: + +```json +{ + "hashAlgorithm": "SHA-256", + "payloadHash": "Base64...", + "tamperProtectionAlgorithm": "HMAC-SHA256", + "tamperProtectionValue": "Base64..." +} +``` + +字段要求如下: + +- `payloadHash` 用于校验明文载荷的一致性 +- `tamperProtectionValue` 用于防篡改校验 +- 恢复前必须先校验 `payloadHash` 与 `tamperProtectionValue` + +### 23.6 加密结构 + +当 `encrypted = true` 时,必须包含 `encryption` 对象和 `payloadCiphertext` 字段。 + +`encryption` 对象建议结构如下: + +```json +{ + "algorithm": "implementation-defined", + "keyId": "default", + "nonce": "Base64...", + "tag": "Base64..." +} +``` + +字段说明: + +- `algorithm` + - 含义:加密算法标识,具体实现可由程序定义 +- `keyId` + - 含义:密钥标识 +- `nonce` + - 含义:随机数或初始向量 +- `tag` + - 含义:认证标签 +- `payloadCiphertext` + - 含义:加密后的差异载荷,使用 Base64 编码 + +### 23.7 明文载荷 `payload` 结构 + +当 `encrypted = false` 时,`payload` 建议结构如下: + +```json +{ + "statistics": { + "textReplaceCount": 0, + "imageReplaceCount": 0 + }, + "operations": [] +} +``` + +字段要求如下: + +- `statistics` + - 含义:统计信息 +- `operations` + - 类型:`array` + - 含义:按实际执行顺序记录的替换操作列表 + - 当文件名发生变化时,必须包含一条文件名变更操作:`type = "fileName"`,`objectType = "documentFileName"`,`originalText` 为原始文件名,`writtenText` 为实际替换后文件名 + +### 23.8 位置对象结构 + +所有操作对象都必须包含 `position` 字段。 + +`position` 对象建议结构如下: + +```json +{ + "storyType": "main", + "containerPath": "/body/table[0]/row[1]/cell[2]", + "paragraphIndex": 3, + "startRunIndex": 1, + "startCharOffset": 4, + "endRunIndex": 2, + "endCharOffset": 7, + "objectIndex": null +} +``` + +字段说明: + +- `storyType` + - 类型:`string` + - 示例值: + - `main` + - `header` + - `footer` + - `comment` + - `textbox` + - `shape` + - `chart` + - `equation` +- `containerPath` + - 类型:`string` + - 含义:逻辑容器路径 +- `paragraphIndex` + - 类型:`integer` + - 含义:容器内段落索引 +- `startRunIndex` + - 类型:`integer` + - 含义:起始 Run 索引 +- `startCharOffset` + - 类型:`integer` + - 含义:起始字符偏移 +- `endRunIndex` + - 类型:`integer` + - 含义:结束 Run 索引 +- `endCharOffset` + - 类型:`integer` + - 含义:结束字符偏移 +- `objectIndex` + - 类型:`integer | null` + - 含义:非文本对象或同容器内对象索引 + +### 23.9 文本替换操作对象结构 + +文本替换操作对象建议结构如下: + +```json +{ + "operationId": "T000001", + "type": "text", + "objectType": "paragraph", + "position": {}, + "rule": { + "id": "R001", + "name": "替换合同编号", + "matchMode": "plain", + "pattern": "合同编号", + "hitIndex": 1 + }, + "originalText": "合同编号", + "replacementTemplateResult": "文件编号", + "writtenText": "文件编号 ", + "displayWidthOriginal": 8, + "displayWidthWritten": 8, + "truncated": false, + "paddingApplied": true +} +``` + +字段说明: + +- `operationId` + - 类型:`string` + - 含义:操作唯一标识 +- `type` + - 类型:`string` + - 固定值:`text` +- `objectType` + - 类型:`string` + - 示例值: + - `paragraph` + - `tableCell` + - `textbox` + - `comment` + - `hyperlinkDisplay` + - `fieldResult` + - `equationText` + - `chartText` +- `rule` + - 类型:`object` + - 含义:命中的规则信息 +- `originalText` + - 类型:`string` + - 含义:替换前文本 +- `replacementTemplateResult` + - 类型:`string` + - 含义:规则替换模板展开后的结果,尚未进行截断或补齐 +- `writtenText` + - 类型:`string` + - 含义:实际写入 `[B]` 的文本 +- `displayWidthOriginal` + - 类型:`integer` + - 含义:原文本显示宽度 +- `displayWidthWritten` + - 类型:`integer` + - 含义:写入文本显示宽度 +- `truncated` + - 类型:`boolean` + - 含义:是否发生截断 +- `paddingApplied` + - 类型:`boolean` + - 含义:是否发生补齐 + +### 23.10 图像替换操作对象结构 + +图像替换操作对象建议结构如下: + +```json +{ + "operationId": "I000001", + "type": "image", + "objectType": "inlineImage", + "position": {}, + "sourceKind": "bitmap", + "originalImage": { + "format": "png", + "widthPx": 800, + "heightPx": 600, + "data": "Base64..." + }, + "convertedBitmapImage": null, + "newImage": { + "format": "png", + "widthPx": 800, + "heightPx": 600, + "data": "Base64..." + }, + "seed": 123456789, + "noiseSummary": { + "modifiedPixelRatio": 0.53 + }, + "layout": { + "displayWidthEmu": 3657600, + "displayHeightEmu": 2743200, + "wrapMode": "inline", + "rotationDegree": 0 + } +} +``` + +字段说明: + +- `type` + - 固定值:`image` +- `objectType` + - 示例值: + - `inlineImage` + - `floatingImage` + - `headerImage` + - `textboxImage` + - `shapeImage` +- `sourceKind` + - 可选值: + - `bitmap` + - `convertedFromVector` +- `originalImage` + - 含义:原始图像数据 +- `convertedBitmapImage` + - 含义:当原对象为非位图时,记录转换后的位图数据;位图源对象时为 `null` +- `newImage` + - 含义:替换后图像数据 +- `seed` + - 含义:用于生成噪声的随机种子 +- `noiseSummary.modifiedPixelRatio` + - 含义:修改像素比例,必须大于等于 `0.5` +- `layout` + - 含义:用于校验图像占位与布局属性未变化 + +### 23.11 `payload` 示例 + +```json +{ + "statistics": { + "textReplaceCount": 1, + "imageReplaceCount": 1 + }, + "operations": [ + { + "operationId": "T000001", + "type": "text", + "objectType": "paragraph", + "position": { + "storyType": "main", + "containerPath": "/body", + "paragraphIndex": 0, + "startRunIndex": 0, + "startCharOffset": 0, + "endRunIndex": 0, + "endCharOffset": 4, + "objectIndex": null + }, + "rule": { + "id": "R001", + "name": "替换合同编号", + "matchMode": "plain", + "pattern": "合同编号", + "hitIndex": 1 + }, + "originalText": "合同编号", + "replacementTemplateResult": "文件编号", + "writtenText": "文件编号", + "displayWidthOriginal": 8, + "displayWidthWritten": 8, + "truncated": false, + "paddingApplied": false + }, + { + "operationId": "I000001", + "type": "image", + "objectType": "inlineImage", + "position": { + "storyType": "main", + "containerPath": "/body", + "paragraphIndex": 2, + "startRunIndex": 0, + "startCharOffset": 0, + "endRunIndex": 0, + "endCharOffset": 0, + "objectIndex": 0 + }, + "sourceKind": "bitmap", + "originalImage": { + "format": "png", + "widthPx": 800, + "heightPx": 600, + "data": "Base64..." + }, + "convertedBitmapImage": null, + "newImage": { + "format": "png", + "widthPx": 800, + "heightPx": 600, + "data": "Base64..." + }, + "seed": 123456789, + "noiseSummary": { + "modifiedPixelRatio": 0.53 + }, + "layout": { + "displayWidthEmu": 3657600, + "displayHeightEmu": 2743200, + "wrapMode": "inline", + "rotationDegree": 0 + } + } + ] +} +``` + +## 24. 附录 C:会话配置文件 `config.json` 结构 + +### 24.1 总体结构 + +`config.json` 采用 JSON 对象作为顶层结构,建议如下: + +```json +{ + "format": "dcit-config", + "version": "1.0", + "savedAt": "2026-04-24T21:00:00+08:00", + "settings": { + "ruleFilePath": "D:\\rules\\default.rule", + "jsonDiffEncryption": true, + "autoExtract": { + "enabledTypes": { + "keyword": true, + "highFrequencyWord": true, + "highFrequencySentence": true + }, + "keywordAlgorithms": ["frequency", "tfIdf", "textRank"], + "maxKeywordCount": 10, + "maxHighFrequencyWordCount": 10, + "maxHighFrequencySentenceCount": 10, + "wordStatisticsOptions": { + "chineseToken": true, + "englishWord": true + }, + "sentenceStatisticsOptions": { + "chineseSentence": true, + "englishSentence": true + }, + "minChineseTokenLength": 1, + "minEnglishWordLength": 1, + "sentenceDelimiters": ["。", "!", "?", ".", "!", "?", ";", ";"], + "excludeEmailAddress": false + } + }, + "replacePage": { + "sourceFolder": "D:\\input", + "replaceFolder": "D:\\output_replace" + }, + "restorePage": { + "replaceFolder": "D:\\output_replace", + "restoreFolder": "D:\\output_restore" + } +} +``` + +### 24.2 顶层字段定义 + +- `format` + - 类型:`string` + - 必填:是 + - 固定值:`dcit-config` +- `version` + - 类型:`string` + - 必填:是 + - 含义:配置文件结构版本 +- `savedAt` + - 类型:`string` + - 必填:是 + - 含义:配置保存时间,采用 ISO 8601 格式 +- `settings` + - 类型:`object` + - 必填:是 + - 含义:全局设置 +- `replacePage` + - 类型:`object` + - 必填:是 + - 含义:替换页面会话信息 +- `restorePage` + - 类型:`object` + - 必填:是 + - 含义:恢复页面会话信息 + +### 24.3 `settings` 对象结构 + +`settings` 对象建议结构如下: + +```json +{ + "ruleFilePath": "D:\\rules\\default.rule", + "jsonDiffEncryption": true, + "autoExtract": { + "enabledTypes": { + "keyword": true, + "highFrequencyWord": true, + "highFrequencySentence": true + }, + "keywordAlgorithms": ["frequency", "tfIdf", "textRank"], + "maxKeywordCount": 10, + "maxHighFrequencyWordCount": 10, + "maxHighFrequencySentenceCount": 10, + "wordStatisticsOptions": { + "chineseToken": true, + "englishWord": true + }, + "sentenceStatisticsOptions": { + "chineseSentence": true, + "englishSentence": true + }, + "minChineseTokenLength": 1, + "minEnglishWordLength": 1, + "sentenceDelimiters": [ + "。", + "!", + "?", + ".", + "!", + "?", + ";", + ";" + ], + "excludeEmailAddress": false + } +} +``` + +字段定义如下: + +- `ruleFilePath` + - 类型:`string` + - 必填:否 + - 含义:当前规则文件的绝对路径 +- `jsonDiffEncryption` + - 类型:`boolean` + - 必填:是 + - 含义:是否默认对 `*.diff` 采用加密存储 +- `autoExtract` + - 类型:`object` + - 必填:否 + - 含义:自动提取相关会话设置 + +`autoExtract` 对象字段定义如下: + +- `enabledTypes` + - 类型:`object` + - 必填:是 + - 含义:自动提取类别开关 +- `keywordAlgorithms` + - 类型:`array` + - 必填:是 + - 默认值:`["frequency", "tfIdf", "textRank"]` + - 允许值:`frequency`、`tfIdf`、`textRank` + - 含义:关键字提取算法集合,按配置顺序轮询执行 +- `maxKeywordCount` + - 类型:`integer` + - 必填:是 + - 默认值:`10` +- `maxHighFrequencyWordCount` + - 类型:`integer` + - 必填:是 + - 默认值:`10` +- `maxHighFrequencySentenceCount` + - 类型:`integer` + - 必填:是 + - 默认值:`10` +- `wordStatisticsOptions` + - 类型:`object` + - 必填:是 + - 含义:高频词统计选项 +- `sentenceStatisticsOptions` + - 类型:`object` + - 必填:是 + - 含义:高频句统计选项 +- `minChineseTokenLength` + - 类型:`integer` + - 必填:是 + - 默认值:`1` +- `minEnglishWordLength` + - 类型:`integer` + - 必填:是 + - 默认值:`1` +- `sentenceDelimiters` + - 类型:`array` + - 必填:是 + - 含义:句子分隔符列表 +- `excludeEmailAddress` + - 类型:`boolean` + - 必填:是 + - 默认值:`false` + - 含义:是否在自动提取中排除邮箱地址 + +### 24.4 `replacePage` 对象结构 + +`replacePage` 对象建议结构如下: + +```json +{ + "sourceFolder": "D:\\input", + "replaceFolder": "D:\\output_replace" +} +``` + +字段定义如下: + +- `sourceFolder` + - 类型:`string` + - 必填:否 + - 含义:替换页面当前原始文件夹绝对路径 +- `replaceFolder` + - 类型:`string` + - 必填:否 + - 含义:替换页面当前替换文件夹绝对路径 + +### 24.5 `restorePage` 对象结构 + +`restorePage` 对象建议结构如下: + +```json +{ + "replaceFolder": "D:\\output_replace", + "restoreFolder": "D:\\output_restore" +} +``` + +字段定义如下: + +- `replaceFolder` + - 类型:`string` + - 必填:否 + - 含义:恢复页面当前替换文件夹绝对路径 +- `restoreFolder` + - 类型:`string` + - 必填:否 + - 含义:恢复页面当前恢复文件夹绝对路径 + +### 24.6 配置文件保存与加载约束 + +- `config.json` 必须使用 UTF-8 编码 +- 所有路径字段均采用绝对路径 +- 缺失字段按默认值处理 +- 文件缺失时,系统可按默认会话配置启动,并在首次保存时生成 +- 文件损坏或无法解析时,系统必须提示用户并阻止执行型任务,直到用户确认重建默认配置或修复原配置文件 + +## 25. 附录 D:日志格式与日志样例 + +### 25.1 日志文件基本要求 + +日志文件采用纯文本格式: + +- 编码:UTF-8 +- 行结束符:CRLF +- 按天追加写入 +- 保存于启动目录 + +### 25.2 单行日志格式 + +建议单行日志采用如下格式: + +```text +[] [] [Job=] [File=""] [Rule=] +``` + +字段说明如下: + +- `` + - 可选值:`INFO`、`WARNING`、`ERROR` +- `` + - 含义:本地时间戳,精确到毫秒 +- `` + - 含义:事件编码 +- `[Job=]` + - 可选字段 + - 含义:批处理任务或文件任务标识 +- `[File=""]` + - 可选字段 + - 含义:相关文件绝对路径 +- `[Rule=]` + - 可选字段 + - 含义:相关规则 ID +- `` + - 含义:日志正文 + +### 25.3 事件编码建议 + +日志事件编码至少建议包含: + +- `APP_START` +- `APP_EXIT` +- `CONFIG_LOAD` +- `CONFIG_SAVE` +- `RULE_LOAD` +- `RULE_SAVE` +- `RULE_CHECK_OK` +- `RULE_CHECK_WARN` +- `RULE_CHECK_ERROR` +- `EXTRACT_START` +- `EXTRACT_PREVIEW` +- `EXTRACT_APPEND` +- `EXTRACT_UNDO` +- `EXTRACT_CANCEL` +- `EXTRACT_EMPTY` +- `EXTRACT_DONE` +- `EXTRACT_SKIP` +- `SCAN_START` +- `SCAN_DONE` +- `FILE_START` +- `TEXT_MATCH` +- `TEXT_WRITE` +- `IMAGE_REPLACE` +- `DIFF_WRITE` +- `BMP_WRITE` +- `RESTORE_START` +- `RESTORE_APPLY` +- `FILE_SUCCESS` +- `FILE_FAIL` +- `FILE_STOPPED` +- `SUMMARY` +- `STACK` + +### 25.4 异常堆栈记录格式 + +当需要记录异常堆栈时: + +- 先输出一条主错误日志 +- 再按堆栈行逐行输出 `STACK` 事件 + +示例: + +```text +[ERROR] 20260424:210512.314 [FILE_FAIL] [Job=J0008] [File="D:\input\a.docx"] 文件处理失败:无法写入差异文件 +[ERROR] 20260424:210512.315 [STACK] [Job=J0008] IOException: access denied +[ERROR] 20260424:210512.316 [STACK] [Job=J0008] at DiffWriter.Commit(...) +``` + +### 25.5 日志样例 + +普通启动样例: + +```text +[INFO] 20260424:210000.102 [APP_START] 程序启动 +[INFO] 20260424:210000.130 [CONFIG_LOAD] 配置加载成功 +[INFO] 20260424:210001.004 [RULE_LOAD] [File="D:\rules\default.rule"] 规则文件加载成功,共 12 条规则 +``` + +扫描样例: + +```text +[INFO] 20260424:210005.217 [SCAN_START] [File="D:\input"] 开始扫描目录 +[INFO] 20260424:210006.884 [SCAN_DONE] [File="D:\input"] 扫描完成,共发现 36 个文件 +``` + +文字匹配样例: + +```text +[INFO] 20260424:210015.011 [FILE_START] [Job=J0001] [File="D:\input\sample.docx"] 开始处理文件 +[INFO] 20260424:210015.428 [TEXT_MATCH] [Job=J0001] [File="D:\input\sample.docx"] [Rule=R001] 命中位置=/body p=0 start=(0,0) end=(0,4) 原文="合同编号" +[INFO] 20260424:210015.431 [TEXT_WRITE] [Job=J0001] [File="D:\input\sample.docx"] [Rule=R001] 写入文本="文件编号" +``` + +图片替换样例: + +```text +[INFO] 20260424:210016.223 [IMAGE_REPLACE] [Job=J0001] [File="D:\input\sample.docx"] 对象=/body p=2 obj=0 seed=123456789 modifiedPixelRatio=0.53 +``` + +规则检查警告样例: + +```text +[WARNING] 20260424:210020.044 [RULE_CHECK_WARN] [Rule=R008] 规则可能与后续规则重叠命中,允许用户确认后继续 +``` + +批处理汇总样例: + +```text +[INFO] 20260424:210530.992 [SUMMARY] 成功=34 失败=2 跳过=0 文本替换=1280 图片替换=96 文本恢复=0 图片恢复=0 +``` + +## 26. 附录 E:BMP 差异文件编码规则 + +### 26.1 目标 + +本附录定义如何将逻辑等价的明文差异数据无损编码为 `*.bmp` 文件,并确保能够从 `*.bmp` 无损恢复出逻辑等价的 JSON 差异数据。 + +### 26.2 编码输入 + +BMP 编码输入为“逻辑等价的明文差异数据”。 + +具体要求如下: + +- 若当前 `*.diff` 文件为未加密形式,则以其规范化 JSON 结构作为输入 +- 若当前 `*.diff` 文件为加密形式,则在内存中使用等价的明文差异数据作为输入 +- `*.bmp` 不承载 `payloadCiphertext` +- `*.bmp` 不提供保密性,仅提供可逆承载能力 + +### 26.3 规范化 JSON 串行化规则 + +在编码为 BMP 之前,必须先将差异数据序列化为规范化 JSON 字节流。 + +规范化规则如下: + +- 编码为 UTF-8 +- 不写入 BOM +- 对象字段按本 SRS 定义顺序输出 +- 数组元素顺序保持原始执行顺序 +- 不输出无意义空白 +- 字符串按 JSON 标准转义 + +说明: + +- 只要解码后得到的差异数据在结构和语义上与原始差异数据等价,即视为满足要求 + +### 26.4 二进制封装结构 + +规范化 JSON 字节流在映射到像素前,必须先封装为二进制载荷。 + +二进制封装结构定义如下: + +1. `Magic` + - 长度:8 字节 + - 固定 ASCII 内容:`DCITBMP1` +2. `HeaderVersion` + - 长度:2 字节 + - 类型:无符号整数,小端序 + - 固定值:`1` +3. `Flags` + - 长度:2 字节 + - 类型:无符号整数,小端序 + - 当前固定值:`0` +4. `PayloadLength` + - 长度:8 字节 + - 类型:无符号整数,小端序 + - 含义:规范化 JSON 字节流长度 +5. `PayloadSha256` + - 长度:32 字节 + - 含义:规范化 JSON 字节流的 SHA-256 值 +6. `PayloadBytes` + - 长度:`PayloadLength` + - 含义:规范化 JSON 字节流本体 + +封装后的字节流记为 `BlobBytes`。 + +### 26.5 像素映射规则 + +`BlobBytes` 必须映射为 8 位灰度 BMP 图像。 + +映射规则如下: + +- BMP 使用 8 位灰度色板 +- 色板中第 `i` 个颜色的 RGB 值必须为 `(i, i, i)`,其中 `i` 取值范围为 `0..255` +- `BlobBytes` 中每个字节值直接映射为一个像素灰度值 +- 映射顺序为按行优先,从左到右、从上到下 + +### 26.6 图像宽高计算规则 + +为保证编码确定性,BMP 宽高采用如下固定规则: + +- `width = 1024` +- `height = ceil(length(BlobBytes) / 1024)` + +若 `length(BlobBytes) = 0`,则: + +- `width = 1024` +- `height = 1` + +### 26.7 尾部填充规则 + +若最后一个像素行未被 `BlobBytes` 填满,则: + +- 剩余像素全部填充为灰度值 `0` + +说明: + +- 这些尾部填充值不属于逻辑载荷 +- 解码时必须依据 `PayloadLength` 精确截取有效数据 + +### 26.8 BMP 行对齐说明 + +BMP 文件行对齐所引入的字节填充属于 BMP 文件格式自身要求。 + +该部分: + +- 不属于逻辑差异数据 +- 解码时必须忽略 BMP 行对齐填充,仅按像素值恢复 `BlobBytes` + +### 26.9 解码规则 + +从 `*.bmp` 恢复差异数据时,解码流程如下: + +1. 读取 BMP 像素数据 +2. 按行优先顺序恢复灰度字节流 +3. 读取并校验 `Magic` +4. 读取 `HeaderVersion` +5. 读取 `Flags` +6. 读取 `PayloadLength` +7. 读取 `PayloadSha256` +8. 按 `PayloadLength` 截取 `PayloadBytes` +9. 校验 `PayloadSha256` +10. 将 `PayloadBytes` 按 UTF-8 解析为规范化 JSON +11. 反序列化为逻辑等价的差异数据对象 + +若任一步失败,则: + +- 必须判定该 `*.bmp` 文件无效 +- 必须拒绝恢复 +- 必须写入日志 + +### 26.10 `*.bmp` 转 `*.diff` 输出规则 + +当系统需要根据 `*.bmp` 生成等价 `*.diff` 文件时: + +- 输出内容应为规范化后的明文 JSON 差异数据 +- 输出编码为 UTF-8 +- 不写入 BOM +- 输出结构必须满足本 SRS 第 23 章定义 + +### 26.11 BMP 编码示意 + +编码过程示意如下: + +```text +Diff Object + -> Canonical JSON UTF-8 Bytes + -> BlobBytes(Magic + Header + Payload) + -> 8-bit Grayscale Pixel Stream + -> BMP File +``` + +解码过程示意如下: + +```text +BMP File + -> 8-bit Grayscale Pixel Stream + -> BlobBytes + -> Canonical JSON UTF-8 Bytes + -> Diff Object +``` + +## 27. 附录 F:测试用例矩阵 + +### 27.1 目的与使用方式 + +本附录给出本项目的最小可执行测试用例矩阵,用于指导: + +- 开发阶段联调验证 +- 系统测试与回归测试 +- 标准验收样例集执行 +- 交付前功能覆盖检查 + +说明如下: + +- `级别=必测` 表示应纳入正式测试与交付前回归 +- `级别=建议` 表示建议覆盖,但不作为单独阻断项 +- 若单个用例涉及多个对象类型,则应分别在 `*.docx` 与 `*.doc` 上至少各验证一组样例 +- 所有涉及“恢复一致性”的用例,均应同时检查文本一致、对象一致性约束和日志记录完整性 + +### 27.2 核心替换功能测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-RPL-001 | 必测 | 正文普通文本替换 | `*.docx` 正文含普通段落文本,配置普通文本规则 | 成功命中并替换,生成 `[B]`、`*.diff`、`*.bmp`,日志记录命中次数 | +| TC-RPL-002 | 必测 | `*.doc` 基本文本替换 | `*.doc` 文档含普通段落文本,配置普通文本规则 | 成功完成替换,输出文件可打开,日志无未处理异常 | +| TC-RPL-003 | 必测 | 页眉文本替换 | 页眉内含普通文本 | 页眉文本成功替换,恢复后与原文一致 | +| TC-RPL-004 | 必测 | 页眉表格替换 | 页眉中含表格及单元格文字 | 表格内文字成功替换,表格结构保持不变 | +| TC-RPL-005 | 必测 | 文本框文字替换 | 正文和页眉中均含文本框文字 | 文本框内文字成功替换,文本框位置和尺寸不变 | +| TC-RPL-006 | 必测 | 合并单元格表格替换 | 表格存在横向或纵向合并单元格 | 合并单元格结构保持不变,允许跨 `Run` 匹配,不允许跨单元格匹配 | +| TC-RPL-007 | 必测 | 批注内容替换 | 文档存在批注文本 | 仅批注内容变化,作者、时间、状态保持不变 | +| TC-RPL-008 | 必测 | 超链接显示文字替换 | 文档含超链接 | 仅显示文字发生替换,目标 URL 保持不变 | +| TC-RPL-009 | 必测 | 目录项相关文本替换 | 文档含目录及其源标题文本 | 替换完成后自动更新目录,目录显示结果与正文一致 | +| TC-RPL-010 | 必测 | 域结果替换 | 文档含域结果显示文本 | 仅域结果参与替换,域代码不修改 | +| TC-RPL-011 | 必测 | 公式内文字替换 | 文档含可解析公式对象 | 公式中可解析文字成功替换,排版不明显异常 | +| TC-RPL-012 | 必测 | 图表文本替换 | 图表含标题、轴标题、图例、数据标签、数据表 | 所有已定义范围的图表文本均参与替换 | +| TC-RPL-013 | 必测 | 嵌入式位图替换 | 文档含 PNG、JPG、BMP、TIFF、GIF 等位图 | 图片完成噪声替换,尺寸、位置、环绕、锚点、裁剪、旋转保持不变 | +| TC-RPL-014 | 必测 | 浮动图片替换 | 文档含浮动位图图片 | 浮动属性保持不变,图片成功替换 | +| TC-RPL-015 | 必测 | 非位图对象图片替换 | 文档含 WMF、EMF、SVG、Office 形状或等价非位图对象 | 成功转换后按图片策略替换,并在恢复后达到视觉一致 | +| TC-RPL-016 | 必测 | 规则顺序执行 | 两条规则前后存在依赖关系 | 严格按列表顺序执行,后一条允许命中前一条替换结果 | +| TC-RPL-017 | 必测 | 区分大小写开关 | 同一原文大小写混合,规则开启或关闭大小写敏感 | 开启时仅命中大小写完全一致项,关闭时大小写无关命中 | +| TC-RPL-018 | 必测 | 全字匹配开关 | 规则原文为词边界敏感内容 | 开启时仅全字命中,关闭时允许部分命中 | +| TC-RPL-019 | 必测 | 正则捕获组回填 | 配置包含捕获组和替换模板的规则 | 正则命中成功,替换结果符合回填模板 | +| TC-RPL-020 | 必测 | 跨 `Run` 匹配 | 同一段落内视觉连续文本被拆成多个 `Run` | 应成功命中并替换 | +| TC-RPL-021 | 必测 | 不跨段落匹配 | 目标文本被分布在两个段落 | 不应命中,日志中不产生错误 | +| TC-RPL-022 | 必测 | 不跨单元格匹配 | 目标文本分布在两个不同单元格 | 不应命中,表格结构保持不变 | +| TC-RPL-023 | 必测 | 过长替换文本截断 | 新文本显示宽度大于原文本允许宽度 | 应按规则截断,且尽量保持版式不变 | +| TC-RPL-024 | 必测 | 过短替换文本补齐 | 新文本显示宽度小于原文本 | 应采用可逆方式补齐,恢复后可完全回到原文本 | +| TC-RPL-025 | 必测 | 页脚文本替换 | 页脚内含普通文本 | 页脚文本成功替换,恢复后与原文一致 | +| TC-RPL-026 | 必测 | 页脚表格替换 | 页脚中含表格及单元格文字 | 表格内文字成功替换,页脚表格结构保持不变 | +| TC-RPL-027 | 必测 | 页脚文本框替换 | 页脚中含文本框文字 | 文本框内文字成功替换,文本框位置和尺寸保持不变 | +| TC-RPL-028 | 必测 | 页眉页脚图片替换 | 页眉或页脚中含嵌入式或浮动图片 | 图片成功替换,尺寸、位置、环绕和锚点保持不变 | +| TC-RPL-029 | 必测 | 图片随机噪声默认变化 | 相同输入、未设置固定随机种子时执行两次替换 | 两次图片替换结果应存在噪声差异,且均可成功恢复 | +| TC-RPL-030 | 必测 | 固定随机种子可复现 | 对同一输入设置相同固定随机种子重复替换 | 图片替换结果保持一致,且恢复结果一致 | +| TC-RPL-031 | 必测 | 替换输出目录结构一致性 | 原始文件夹包含多级子目录并执行替换 | 替换后文件、`*.diff`、`*.bmp` 均保留相对目录树结构 | +| TC-RPL-032 | 必测 | 替换输出重名自动重命名 | 替换输出目录已存在同名目标文件 | 新输出采用自增序号重命名,不覆盖既有文件;同一源文件的替换文档、`*.diff`、`*.bmp` 自增序号一致 | +| TC-RPL-033 | 必测 | 文件名参与规则替换 | 原始文件名命中启用规则,正文可无命中 | 替换后文件、`*.diff`、`*.bmp` 均使用规则替换后的文件基本名加日期后缀保存;差异元数据和 `payload.operations` 均记录文件名变更 | +| TC-RPL-034 | 必测 | 文件名替换非法结果防护 | 文件名规则替换结果为空或包含非法文件名字符 | 系统阻断当前扫描/执行,日志记录源文件、替换后文件名结果和错误原因 | +| TC-RPL-035 | 必测 | 文件名变更修改记录 | 文件名因规则、日期后缀或同名自增导致最终输出名与原始文件名不同 | 差异文件 `payload.operations` 包含 `type=fileName`、`objectType=documentFileName` 的记录,原始文件名和实际替换后文件名准确 | + +### 27.3 恢复与一致性测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-RST-001 | 必测 | 从 `*.diff` 恢复文本 | 使用替换输出的 `[B] + *.diff` | 成功恢复出 `[A']`,文本内容与原始 `[A]` 完全一致 | +| TC-RST-002 | 必测 | 从 `*.bmp` 恢复文本 | 使用替换输出的 `[B] + *.bmp` | 先解码为等价差异数据,再恢复成功 | +| TC-RST-003 | 必测 | 文本与图片同时恢复 | 文档同时包含文本替换和图片替换 | 文本和图片均恢复成功 | +| TC-RST-004 | 必测 | 严格位置匹配恢复 | 差异文件中的位置与当前文档严格一致 | 正常恢复,不启用容错查找 | +| TC-RST-005 | 必测 | 位置不匹配恢复失败 | 人工修改 `[B]` 后再执行恢复 | 恢复被拒绝,日志明确给出失败原因 | +| TC-RST-006 | 必测 | 目录自动更新 | 恢复后文档含目录 | 恢复完成后自动更新目录显示结果 | +| TC-RST-007 | 必测 | 交叉引用错误验证 | 恢复后文档显示结果中存在“错误!未找到引用源”或等价域错误文本 | 恢复输出被拒绝,日志记录 Story、容器、段落和上下文片段 | +| TC-RST-008 | 必测 | 编号段落一致性 | 原文包含多级编号列表 | 恢复后编号值、级别、缩进、制表位、自动编号行为均一致 | +| TC-RST-009 | 必测 | 表格与文本框一致性 | 原文含表格、页眉表格、文本框 | 恢复后结构、位置、内容与原文一致 | +| TC-RST-010 | 必测 | 图片视觉一致性 | 原文含位图和非位图对象 | 恢复后应满足肉眼一致、尺寸与位置一致,并满足像素相似度阈值要求 | +| TC-RST-011 | 必测 | 路径结构一致性 | 目录批处理替换后执行恢复 | 恢复输出目录保留原目录树结构 | +| TC-RST-012 | 必测 | 重名自动重命名 | 目标恢复目录已有同名输出 | 新输出采用自增序号重命名,不覆盖原文件 | +| TC-RST-013 | 建议 | 多轮替换后恢复验证 | 同一文件多次独立处理,存在多个版本输出 | 每组 `[B] + C` 仅对应恢复其本轮结果,不得串用 | +| TC-RST-014 | 必测 | 单批次差异文件格式不混用 | 同一恢复批次同时勾选 `*.diff` 和 `*.bmp` 两类输入 | 系统拒绝混合执行,提示用户单次批处理仅允许一种差异文件格式 | +| TC-RST-015 | 必测 | 规则文件指纹错配恢复失败 | `[B]` 与差异文件匹配,但差异文件中的规则文件指纹与当前恢复上下文不一致 | 恢复被拒绝,日志明确记录规则文件指纹错配 | +| TC-RST-016 | 必测 | 程序或算法版本不兼容恢复失败 | 差异文件中的程序版本或算法版本与当前程序不兼容 | 恢复被拒绝,日志明确记录版本不兼容原因 | +| TC-RST-017 | 必测 | 恢复原始文件名 | 原始文件名经规则替换后生成 `[B]`,差异文件记录 `sourceDocument.fileName` | 恢复输出使用原始文件名和原始扩展名;若同名冲突则自动追加自增序号 | + +### 27.4 规则文件与差异文件测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-RULE-001 | 必测 | 打开规则文件 | 使用合法 `*.rule` 文件 | 成功加载全部规则,顺序与文件内一致 | +| TC-RULE-002 | 必测 | 保存规则文件 | 编辑后保存 `*.rule` | 成功保存为 UTF-8 JSON,内容满足附录 A 定义 | +| TC-RULE-003 | 必测 | 非法正则禁止保存 | 规则中包含非法正则表达式 | 保存失败,弹窗提示,日志能定位规则位置和详细错误 | +| TC-RULE-004 | 必测 | 规则兼容性检查告警 | 存在重叠命中或重复命中风险 | 弹窗提示并写日志,允许用户继续 | +| TC-RULE-005 | 必测 | 规则兼容性检查阻断 | 存在无法解析或必然错误规则 | 禁止保存或禁止执行,并写日志 | +| TC-RULE-006 | 必测 | 差异文件 JSON 明文保存 | 会话配置为 `jsonDiffEncryption=false` | 输出明文 `*.diff`,结构符合附录 B | +| TC-RULE-007 | 必测 | 差异文件 JSON 加密保存 | 会话配置为 `jsonDiffEncryption=true` | 输出加密 `*.diff`,恢复时可正确解密 | +| TC-RULE-008 | 必测 | BMP 与 JSON 等价性 | 同一替换结果同时生成 `*.diff` 与 `*.bmp` | `*.bmp` 可无损还原出逻辑等价的明文差异数据 | +| TC-RULE-009 | 必测 | 差异文件完整性校验 | 人工篡改 `*.diff` 或 `*.bmp` | 校验失败,拒绝恢复,不允许强制继续 | +| TC-RULE-010 | 必测 | 指纹错配阻断 | 使用不匹配的 `[B]`、规则文件或差异文件组合 | 恢复失败,日志标明错配类型 | +| TC-RULE-011 | 必测 | 自动提取预览流程 | 在替换页面勾选一个或多个文件并点击自动提取 | 基于全部勾选文件生成关键字、高频词、高频句候选结果并展示预览,不直接写入规则列表 | +| TC-RULE-012 | 必测 | 自动提取选择性追加 | 在预览中仅勾选部分候选项 | 仅被勾选的候选项被追加为规则 | +| TC-RULE-013 | 必测 | 自动提取规则默认属性 | 执行自动提取并追加后检查新增规则字段 | 新增规则默认满足“启用=是、普通文本、区分大小写=是、全字匹配=是、原文本=提取结果、新文本为空字符串”,且系统不因新文本为空而自动禁用规则 | +| TC-RULE-014 | 必测 | 自动提取去重 | 当前规则列表中已存在相同原文本规则,再次执行自动提取 | 不重复追加,弹窗和日志给出跳过数量 | +| TC-RULE-015 | 必测 | 自动提取不覆盖人工规则 | 自动提取结果与人工规则原文本相同 | 自动提取结果被丢弃,人工规则保持不变 | +| TC-RULE-016 | 必测 | 自动提取命名与备注 | 执行自动提取并追加后检查新增规则的 ID、名称、备注 | ID 前缀、名称格式和备注内容满足 SRS 要求 | +| TC-RULE-017 | 必测 | 自动提取空结果提示 | 文档无有效候选项 | 弹窗提示“未提取到可用项”,不修改规则列表 | +| TC-RULE-018 | 必测 | 自动提取撤销 | 完成一次自动提取追加后执行撤销 | 最近一次自动提取新增的规则被整体回退 | +| TC-RULE-019 | 必测 | 自动提取关键字算法轮询 | 关键字算法同时勾选按词频、`TF-IDF`、`TextRank` | 系统按配置顺序对勾选文件范围各执行一轮,并对结果去重合并后进入预览 | +| TC-RULE-020 | 必测 | 自动提取邮箱与单字默认纳入 | 使用包含邮箱地址、单字、单词的勾选文件集合执行自动提取 | 邮箱地址参与提取,单字和单词按最小长度设置纳入统计 | +| TC-RULE-021 | 必测 | 执行前规则兼容性复检 | 规则文件已成功保存后,执行前再引入兼容性风险条件 | 启动执行前再次触发规则兼容性检查,弹窗和日志同时给出结果 | +| TC-RULE-022 | 必测 | 自动提取多文件跨文档去重 | 多个勾选文件中存在相同候选原文本 | 预览和追加结果中仅保留一条规则,并保留首次出现文档路径和顺序 | +| TC-RULE-023 | 必测 | 自动提取仅写入会话内存直到保存 | 自动提取追加规则后不执行保存规则文件,直接关闭或重新加载 | 内存规则变化不写入 `*.rule` 文件,原规则文件内容保持不变 | +| TC-RULE-024 | 必测 | 自动提取追加前后双阶段检查 | 自动提取结果中包含重复项和追加后才暴露的兼容性问题 | 追加前执行快速去重和冲突检查,追加后执行完整合法性和兼容性检查 | +| TC-RULE-025 | 必测 | BMP 文件命名与输出位置 | 执行替换并生成差异文件 | `*.bmp` 与 `*.diff` 同名同目录生成 | +| TC-RULE-026 | 必测 | 自动提取范围以勾选集为准 | 文件列表高亮选择与复选框勾选集合不一致 | 自动提取仅扫描勾选文件,高亮选中状态不影响结果 | + +### 27.5 异常与防护测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-EXC-001 | 必测 | 不支持加密文档 | 输入为加密文档 | 当前文件失败并记录错误,后续文件继续 | +| TC-EXC-002 | 必测 | 不支持受保护文档 | 输入为受保护文档 | 当前文件失败并记录错误,后续文件继续 | +| TC-EXC-003 | 必测 | 支持只读文档 | 输入为只读文档 | 可正常读取并处理 | +| TC-EXC-004 | 必测 | 目录重叠阻断 | 原始目录与替换目录或恢复目录相同或互为父子目录 | 禁止执行,弹窗并写日志 | +| TC-EXC-005 | 必测 | 原子写入保护 | 生成 `[B]` 后、写入 `*.diff` 前制造故障 | 不保留正式半成品,仅允许保留临时文件用于清理 | +| TC-EXC-006 | 必测 | 输出文件被占用 | 目标输出文件被其他进程占用 | 当前文件失败并记录原因,后续文件继续 | +| TC-EXC-007 | 必测 | 源文件处理中被修改 | 处理过程中外部修改源文件 | 当前文件失败并记录错误,后续文件继续 | +| TC-EXC-008 | 必测 | 磁盘空间不足 | 输出盘或临时目录空间不足 | 当前文件失败,日志明确记录空间异常 | +| TC-EXC-009 | 必测 | 临时目录不可写 | 临时目录权限不足 | 当前批次禁止启动,提示并写日志 | +| TC-EXC-010 | 必测 | 字体缺失兜底 | 目标机缺少影响排版的关键字体 | 当前文件继续处理,中文/东亚字体使用宋体兜底,英文和数字使用 Times New Roman 兜底,日志记录缺失字体和兜底说明 | +| TC-EXC-011 | 必测 | 零长度匹配防护 | 正则规则可产生零长度命中 | 执行期被阻断或安全跳过,不发生死循环 | +| TC-EXC-012 | 必测 | 灾难性回溯防护 | 构造高风险正则输入 | 执行可控失败或超时终止,程序不假死 | +| TC-EXC-013 | 必测 | 命中次数异常上限防护 | 单规则在单文件内产生异常大量命中 | 触发防护并记录日志 | +| TC-EXC-014 | 必测 | 停止按钮行为 | 执行批处理过程中点击停止 | 尽量完成当前文件后停止,其余未开始文件标记为“已停止” | +| TC-EXC-015 | 必测 | 崩溃后临时文件清理 | 制造异常中断后重启程序 | 启动时发现并清理无效临时文件,日志记录清理过程 | +| TC-EXC-016 | 必测 | 重复入队防护 | 同一源文件因扫描或并发被重复加入任务队列 | 系统应去重,不重复处理同一源文件 | +| TC-EXC-017 | 必测 | 配置文件损坏处理 | `config.json` 内容损坏 | 禁止按损坏配置继续执行,提示并写日志,直到用户确认重建默认配置或修复原配置 | +| TC-EXC-018 | 必测 | 规则文件损坏处理 | `*.rule` 文件内容损坏 | 加载失败并提示,不得带病执行 | +| TC-EXC-019 | 必测 | 自动提取取消保护 | 自动提取过程中执行取消 | 当前规则列表保持不变,日志记录取消事件 | +| TC-EXC-020 | 必测 | 自动提取失败不破坏规则列表 | 自动提取过程中制造解析异常 | 现有规则列表保持不变,日志记录失败原因 | +| TC-EXC-021 | 必测 | 配置文件缺失处理 | 启动时不存在 `config.json` | 系统按默认会话配置启动,不阻断执行,首次保存时生成新的 `config.json` | +| TC-EXC-022 | 必测 | 批处理前日志文件不可创建 | 启动目录日志文件无法创建或打开 | 当前批处理不得启动,并提示运行环境不满足要求 | +| TC-EXC-023 | 必测 | 运行中日志落盘失败 | 批处理过程中日志文件追加写入失败 | 当前文件可控结束,界面日志继续保留,当前批次结束后不再调度新任务 | +| TC-EXC-024 | 必测 | 单文件失败后继续后续文件 | 批处理中首个文件失败,后续文件仍可正常处理 | 当前失败文件被记录,后续文件继续执行,汇总结果正确统计成功和失败数量 | + +### 27.6 UI、日志与会话配置测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-UI-001 | 必测 | 三页签存在性 | 启动程序 | 存在“替换”“恢复”“设置”三个固定页签 | +| TC-UI-002 | 必测 | 现代目录选择对话框 | 点击目录选择按钮 | 弹出文件选择风格的目录选择对话框,而非老式树形浏览目录框 | +| TC-UI-003 | 必测 | 失焦自动扫描 | 修改原始目录输入框后移出焦点 | 自动异步扫描,不阻塞界面 | +| TC-UI-004 | 必测 | 文件列表排序 | 点击文件列表列头 | 列表按所选列排序 | +| TC-UI-005 | 必测 | 进度显示 | 批处理执行中 | 显示总进度条、单文件进度条、当前文件、已处理数量、累计命中数 | +| TC-UI-006 | 必测 | 当前规则显示 | 批处理执行中 | 显示当前规则 ID 和规则名称 | +| TC-UI-007 | 必测 | 行状态颜色 | 文件成功、失败、处理中、已停止 | 颜色分别符合绿色、红色、蓝色、灰色定义 | +| TC-UI-008 | 必测 | 完成汇总弹窗 | 替换或恢复批处理完成 | 弹出汇总结果,包含成功数、失败数、停止数及替换恢复统计 | +| TC-UI-009 | 必测 | 日志编码与内容 | 执行任一处理流程 | 日志文件为 UTF-8,包含路径、规则 ID、事件码、失败原因等字段 | +| TC-UI-010 | 必测 | 异常堆栈日志 | 制造处理期异常 | 日志中先写主错误,再写 `STACK` 事件 | +| TC-UI-011 | 必测 | 会话配置持久化 | 修改 `jsonDiffEncryption` 等设置后重启程序 | 设置写入 `config.json` 并可正确恢复 | +| TC-UI-012 | 建议 | 长日志连续写入 | 长时间批处理大量文件 | 界面持续响应,日志正常追加,不出现明显卡死 | +| TC-UI-013 | 必测 | 自动提取按钮使能条件 | 文件列表分别处于未勾选、勾选一个、勾选多个状态 | 仅在至少勾选一个文件时允许执行自动提取,未勾选时禁用或明确提示 | +| TC-UI-014 | 必测 | 自动提取结果同步显示 | 在替换页面执行自动提取后切换到设置页面 | 新增规则应立即显示在规则列表中 | +| TC-UI-015 | 必测 | 自动提取进度与取消 | 执行大文档自动提取 | 界面显示提取进度,可取消,且界面保持响应 | +| TC-UI-016 | 必测 | 自动提取后自动定位 | 自动提取成功追加规则后 | 自动切换到设置页面并定位到第一条新增规则 | +| TC-UI-017 | 必测 | 自动提取期间禁止编辑规则 | 自动提取进行中尝试编辑规则列表 | 编辑入口被禁用或阻断 | +| TC-UI-018 | 必测 | 自动提取设置持久化 | 修改自动提取设置并重启程序 | 自动提取相关设置从 `config.json` 正确恢复 | +| TC-UI-019 | 必测 | 目录选择取消不修改路径 | 打开目录选择对话框后点击“取消” | 对应路径文本框内容保持不变 | +| TC-UI-020 | 必测 | 文件列表批量勾选与取消勾选 | 在替换页面文件列表执行批量勾选和批量取消勾选 | 复选框状态批量更新正确,自动提取和批处理范围随之变化 | +| TC-UI-021 | 必测 | 恢复页失焦自动扫描 | 修改恢复页相关路径输入框后移出焦点 | 自动异步扫描可恢复文件,不阻塞界面 | +| TC-UI-022 | 必测 | 自动提取预览字段完整性 | 执行自动提取进入预览界面 | 预览列表至少显示是否追加、提取类型、原文本、统计值、首次出现文档路径、首次出现顺序 | +| TC-UI-023 | 必测 | 会话配置自动刷新 | 修改会话配置项但不重启程序 | `config.json` 随配置变化自动刷新,内容与当前会话一致 | +| TC-UI-024 | 必测 | 日志框右键菜单 | 在日志框打开右键菜单并执行复制全部内容、清空日志 | 菜单项完整,复制操作覆盖全部日志文本,清空操作仅清空界面日志框 | +| TC-UI-025 | 必测 | 替换页文件列表右键批量操作 | 在替换页选择多行后执行删除、启用、不启用 | 所选行被批量删除或批量更新勾选状态,界面状态同步刷新 | +| TC-UI-026 | 必测 | 恢复页文件列表右键批量操作 | 在恢复页选择多行后执行删除、启用、不启用 | 所选行被批量删除或批量更新勾选状态,界面状态同步刷新 | +| TC-UI-027 | 必测 | 规则列表右键批量操作 | 在设置页规则列表选择多行后执行删除、启用、不启用 | 所选规则被批量删除或批量更新启用状态,规则列表与执行状态同步刷新 | + +### 27.7 兼容性与运行环境测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-CMP-001 | 必测 | Win7 SP1 x64 兼容性 | 在 Win7 SP1 x64 环境运行绿色版 | 程序可启动、可执行基本替换与恢复流程 | +| TC-CMP-002 | 必测 | x64 平台约束 | 在 x64 目标机运行 | 程序可正常运行 | +| TC-CMP-003 | 必测 | 无管理员权限运行 | 目标机普通用户权限,无管理员权限 | 程序可正常启动并完成处理 | +| TC-CMP-004 | 必测 | 无软件安装权限运行 | 目标机禁止安装软件 | 绿色版可直接运行,不依赖安装流程 | +| TC-CMP-005 | 必测 | 启动目录可写要求 | 启动目录可写 | 程序可正常创建日志与配置文件 | +| TC-CMP-006 | 必测 | 启动目录不可写阻断 | 启动目录不可写 | 视为环境不满足,程序拒绝继续运行并提示原因 | +| TC-CMP-007 | 建议 | 首次启动解压内置组件 | 完全离线环境首次运行 | 可成功解压所需组件并进入可用状态 | +| TC-CMP-008 | 必测 | Word/WPS 宿主机前提 | 替换/恢复宿主机安装支持 docx 的 Word 或 WPS | 程序可调用宿主能力完成域刷新、doc/docx 转换或相关文档处理 | +| TC-CMP-009 | 必测 | 无 Visio 依赖 | 文档包含 Visio/OLE 对象且目标机未安装 Visio | 按可见位图处理和恢复,不要求安装 Visio | + +### 27.8 响应性与目标性能测试矩阵 + +| 用例 ID | 级别 | 测试目标 | 输入或前置条件 | 预期结果 | +| --- | --- | --- | --- | --- | +| TC-PRF-001 | 建议 | 100 页替换目标性能 | 单个 100 页文档执行替换 | 目标值为不超过 10 秒;若未达到,不单独作为强制阻断项 | +| TC-PRF-002 | 建议 | 100 页恢复目标性能 | 单个 100 页文档执行恢复 | 目标值为不超过 10 秒;若未达到,不单独作为强制阻断项 | +| TC-PRF-003 | 必测 | 大文件界面响应性 | 单个大文件处理过程中观察界面 | 不得出现界面卡死、状态不更新、操作无响应 | +| TC-PRF-004 | 必测 | 多文件批处理响应性 | 多文件批处理执行过程中观察界面 | 不得出现界面卡死、状态不更新、操作无响应 | +| TC-PRF-005 | 建议 | 并发处理稳定性 | 开启多文件并行处理 | 应能利用环境资源并保持正确性,不出现重复处理、输出冲突或死锁 | + +### 27.9 标准验收样例集覆盖要求 + +标准验收样例集至少应覆盖以下样例类别,并与本章测试矩阵建立一一映射关系: + +- `*.docx` 正文、页眉、页脚 +- `*.doc` 正文、页眉、页脚 +- 文本框、批注、表格、合并单元格 +- 超链接显示文字、目录、域结果 +- 公式、图表 +- 位图图片、非位图图片 +- 嵌入式图片、浮动图片 +- 自动提取关键字、高频词、高频句 +- 规则顺序、正则、大小写、全字匹配、跨 `Run` +- 恢复严格匹配、差异错配、完整性校验失败 +- 无管理员权限、无安装权限、启动目录可写与不可写场景 + +测试报告中至少应包含以下映射信息: + +- 样例编号 +- 对应用例 ID +- 输入文件路径 +- 规则文件路径 +- 执行时间 +- 结果判定 +- 失败原因 +- 日志位置 diff --git a/使用说明.docx b/使用说明.docx new file mode 100644 index 0000000..45b59e8 Binary files /dev/null and b/使用说明.docx differ diff --git a/打包命令.txt b/打包命令.txt new file mode 100644 index 0000000..dbabc74 --- /dev/null +++ b/打包命令.txt @@ -0,0 +1 @@ +dotnet publish DCIT.App/DCIT.App.csproj /p:PublishProfile=FolderProfile \ No newline at end of file