first commit

This commit is contained in:
lihansani
2026-07-13 10:04:36 +08:00
commit 577aa128bd
870 changed files with 25849 additions and 0 deletions

View File

@@ -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(
@"(?<Email>[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})|(?<English>[A-Za-z][A-Za-z0-9_\-']*)|(?<Chinese>[\u4E00-\u9FFF]+)",
RegexOptions.Compiled);
private readonly WordTextExtractionService _wordTextExtractionService;
public AutoExtractService(WordTextExtractionService wordTextExtractionService)
{
_wordTextExtractionService = wordTextExtractionService;
}
public Task<AutoExtractResult> ExtractAsync(
IList<string> selectedFiles,
AutoExtractSettings settings,
IList<RuleDefinition> existingRules,
IProgress<ExtractionProgress> progress,
CancellationToken cancellationToken)
{
return Task.Run(
() => Execute(selectedFiles, settings, existingRules, progress, cancellationToken),
cancellationToken);
}
private AutoExtractResult Execute(
IList<string> selectedFiles,
AutoExtractSettings settings,
IList<RuleDefinition> existingRules,
IProgress<ExtractionProgress> progress,
CancellationToken cancellationToken)
{
var result = new AutoExtractResult();
var extractedDocuments = new List<ExtractedDocumentText>();
if (selectedFiles == null || selectedFiles.Count == 0)
{
return result;
}
settings = settings ?? new AutoExtractSettings();
existingRules = existingRules ?? Array.Empty<RuleDefinition>();
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<string, ExtractionCandidate>(StringComparer.Ordinal);
var existingSearchTexts = new HashSet<string>(
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<ExtractionCandidate> BuildKeywordCandidates(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
{
var tokenObservations = BuildTokenObservations(documents, settings).ToList();
if (tokenObservations.Count == 0)
{
return Enumerable.Empty<ExtractionCandidate>();
}
var candidates = new List<ExtractionCandidate>();
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<ExtractionCandidate> BuildHighFrequencyWordCandidates(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
{
var tokenObservations = BuildTokenObservations(documents, settings).ToList();
return CreateCandidatesFromRankedEntries(
RankByFrequency(tokenObservations).Take(settings.MaxWordCount),
AutoExtractCategory.HighFrequencyWord,
"高频词");
}
private IEnumerable<ExtractionCandidate> BuildSentenceCandidates(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
{
var sentenceMap = new Dictionary<string, RankedEntry>(StringComparer.Ordinal);
var order = 0;
var delimiters = new HashSet<char>((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<TokenObservation> BuildTokenObservations(IList<ExtractedDocumentText> documents, AutoExtractSettings settings)
{
var observations = new List<TokenObservation>();
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<string> 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<RankedEntry> RankByFrequency(IList<TokenObservation> 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<RankedEntry> RankByTfIdf(IList<TokenObservation> observations)
{
var documentCount = observations.Select(item => item.DocumentIndex).Distinct().Count();
var byToken = observations.GroupBy(item => item.Token, StringComparer.Ordinal);
var ranked = new List<RankedEntry>();
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<RankedEntry> RankByTextRank(IList<TokenObservation> observations)
{
var graph = new Dictionary<string, HashSet<string>>(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<string>(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<string>(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<string, double>(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<ExtractionCandidate> CreateCandidatesFromRankedEntries(
IEnumerable<RankedEntry> 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<string> SplitSentences(string text, HashSet<char> 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<string, ExtractionCandidate> candidateMap,
ISet<string> 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; }
}
}
}

View File

@@ -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<FileScanItem> selectedItems, int processorCount)
{
var executionItems = new List<FileScanItem>();
var skippedItems = new List<BatchExecutionSkipItem>();
var sourcePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var outputPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var diffPaths = new HashSet<string>(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<string> 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<string> 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();
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<AppConfig>(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();
}
}
}

View File

@@ -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<DiffDocument>(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<DiffDocument>(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<DiffPayload>(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<DiffDocument>(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<DiffDocument>(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<DiffPayload>(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; }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<FileScanItem> ScanReplaceCandidates(string sourceDirectory, string outputDirectory)
{
return ScanReplaceCandidates(sourceDirectory, outputDirectory, null);
}
public IList<FileScanItem> ScanReplaceCandidates(string sourceDirectory, string outputDirectory, RuleFile ruleFile)
{
var items = new List<FileScanItem>();
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<FileScanItem> 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<string>();
var diffLookup = selectedDiffs.ToDictionary(
path => BuildRestoreKey(replaceDirectory, path),
StringComparer.OrdinalIgnoreCase);
var wordFiles = EnumerateWordFiles(replaceDirectory);
var items = new List<FileScanItem>();
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<string> ValidateReplaceDirectories(string sourceDirectory, string outputDirectory)
{
return PathUtility.ValidateDirectoryPair("原始文件夹", sourceDirectory, "替换文件夹", outputDirectory).ToList();
}
public IList<string> ValidateRestoreDirectories(string replaceDirectory, string restoreDirectory)
{
return PathUtility.ValidateDirectoryPair("替换文件夹", replaceDirectory, "恢复文件夹", restoreDirectory).ToList();
}
private static IEnumerable<string> EnumerateWordFiles(string rootDirectory)
{
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory))
{
return Enumerable.Empty<string>();
}
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<RuleDefinition> GetEnabledRules(RuleFile ruleFile, RuleTarget target)
{
if (ruleFile == null || ruleFile.Rules == null)
{
return new List<RuleDefinition>();
}
return ruleFile.Rules
.Where(rule => rule != null && rule.IsEnabled && !string.IsNullOrEmpty(rule.SearchText) && rule.Target == target)
.ToList();
}
public static string ApplyRulesToFileName(string fileNameWithoutExtension, IList<RuleDefinition> 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);
}
}
}

View File

@@ -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<Dictionary<string, HashSet<string>>> KnownFontAliases =
new Lazy<Dictionary<string, HashSet<string>>>(BuildKnownFontAliases);
private readonly Lazy<HashSet<string>> _installedFonts;
public FontValidationService()
: this(null)
{
}
public FontValidationService(IEnumerable<string> installedFonts)
{
_installedFonts = new Lazy<HashSet<string>>(
delegate
{
return installedFonts == null
? LoadInstalledFonts()
: new HashSet<string>(
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<string>(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<string> missingFonts)
{
var missingFontSet = new HashSet<string>(
(missingFonts ?? Array.Empty<string>())
.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<W.RunFonts>())
{
changed |= ApplyWordRunFontFallbacks(runFonts, missingFontSet, themeFonts);
}
foreach (var element in root.Descendants())
{
changed |= ApplyDrawingFontFallback(element, missingFontSet);
}
if (changed)
{
root.Save();
}
}
}
}
private static IEnumerable<OpenXmlPartRootElement> EnumerateFontFallbackRoots(WordprocessingDocument document)
{
var main = document.MainDocumentPart;
if (main == null)
{
yield break;
}
if (main.Document != null)
{
yield return main.Document;
}
var queue = new Queue<OpenXmlPartContainer>();
var visited = new HashSet<OpenXmlPart>();
queue.Enqueue(main);
while (queue.Count > 0)
{
var container = queue.Dequeue();
foreach (var pair in container.Parts)
{
var part = pair.OpenXmlPart;
if (part == null || !visited.Add(part))
{
continue;
}
if (part.RootElement != null)
{
yield return part.RootElement;
}
queue.Enqueue(part);
}
}
}
private static bool ApplyWordRunFontFallbacks(
W.RunFonts runFonts,
ISet<string> missingFonts,
IDictionary<string, string> 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<string> themeLocalNames,
string fallbackFont,
ISet<string> missingFonts,
IDictionary<string, string> 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<string> 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<string> localNames)
{
var names = new HashSet<string>(
(localNames ?? Array.Empty<string>()).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<string> LoadInstalledFonts()
{
var collection = new InstalledFontCollection();
return new HashSet<string>(
collection.Families
.Select(item => NormalizeFontName(item.Name))
.Where(name => !string.IsNullOrWhiteSpace(name)),
StringComparer.OrdinalIgnoreCase);
}
private static Dictionary<string, HashSet<string>> BuildDocumentFontAliasMap(WordprocessingDocument document)
{
var result = new Dictionary<string, HashSet<string>>(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<W.Font>())
{
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<OpenXmlPartRootElement> EnumerateRelevantRoots(WordprocessingDocument document)
{
var main = document.MainDocumentPart;
if (main == null)
{
yield break;
}
if (main.Document != null)
{
yield return main.Document;
}
var queue = new Queue<OpenXmlPartContainer>();
var visited = new HashSet<OpenXmlPart>();
queue.Enqueue(main);
while (queue.Count > 0)
{
var container = queue.Dequeue();
foreach (var pair in container.Parts)
{
var part = pair.OpenXmlPart;
if (part == null || !visited.Add(part))
{
continue;
}
if (part.RootElement != null && 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<string, StyleFontSet> BuildStyleIndex(WordprocessingDocument document)
{
var result = new Dictionary<string, StyleFontSet>(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<W.Style>())
{
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<string, string> 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<string, string> BuildThemeFontMap(WordprocessingDocument document)
{
var result = new Dictionary<string, string>(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<string, string> 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<string, StyleFontSet> styleIndex,
FontReferenceSet defaultFonts,
IDictionary<string, string> themeFonts,
ISet<string> usedFonts)
{
foreach (var run in root.Descendants<W.Run>())
{
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<string> 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<W.DeletedRun>().Any())
{
return false;
}
return run.RunProperties == null || run.RunProperties.Vanish == null;
}
private static FontReferenceSet ResolveEffectiveWordFonts(
W.Run run,
IDictionary<string, StyleFontSet> styleIndex,
FontReferenceSet defaultFonts,
IDictionary<string, string> themeFonts)
{
var effectiveFonts = new FontReferenceSet();
effectiveFonts.ApplyOverride(defaultFonts);
var paragraph = run.Ancestors<W.Paragraph>().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<W.Table>().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<string, StyleFontSet> 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<string>(StringComparer.OrdinalIgnoreCase));
result.ApplyOverride(resolved);
return result;
}
private static FontReferenceSet ResolveStyleFonts(
string styleId,
IDictionary<string, StyleFontSet> styleIndex,
ISet<string> 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<string, string> 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<string, string> 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<string, HashSet<string>> documentFontAliases)
{
if (string.IsNullOrWhiteSpace(fontName))
{
return true;
}
var installedFonts = _installedFonts.Value;
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var queue = new Queue<string>();
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<string, HashSet<string>> BuildKnownFontAliases()
{
var result = new Dictionary<string, HashSet<string>>(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<string, HashSet<string>> aliases, Queue<string> 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<string, HashSet<string>> 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<string, HashSet<string>> aliases, string key, string value)
{
if (!aliases.TryGetValue(key, out var values))
{
values = new HashSet<string>(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<string> usedFonts, IList<string> missingFonts)
{
UsedFonts = usedFonts ?? Array.Empty<string>();
MissingFonts = missingFonts ?? Array.Empty<string>();
}
public IList<string> UsedFonts { get; private set; }
public IList<string> 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<string> 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;
}
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DCIT.Core.Services
{
public interface IDocConversionService
{
void Convert(string sourcePath, string outputPath);
}
}

View File

@@ -0,0 +1,7 @@
namespace DCIT.Core.Services
{
public interface IDocumentFieldUpdateService
{
void UpdateDynamicContent(string documentPath);
}
}

View File

@@ -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<FileProcessProgress> progress, CancellationToken cancellationToken);
FileProcessResult Restore(RestoreFileRequest request, IProgress<FileProcessProgress> progress, CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,9 @@
namespace DCIT.Core.Services
{
public interface IFontValidationService
{
FontValidationResult Validate(string docxPath);
FontValidationResult EnsureFontsAvailable(string docxPath);
}
}

View File

@@ -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<LogEntry> EntryWritten;
public event EventHandler<LogWriteFailureEventArgs> 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));
}
}
}

View File

@@ -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<string>();
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<string>();
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<string> GetOfficeAutomationProgIds()
{
yield return "Word.Application";
foreach (var progId in WpsProgIds)
{
yield return progId;
}
}
protected virtual string ResolveDocToPath()
{
var candidates = new List<string>();
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
{
}
}
}
}

View File

@@ -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<RuleFile>(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<string>(StringComparer.OrdinalIgnoreCase);
var validatedRules = new List<ValidatedRuleContext>();
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<RuleDefinition> existingRules)
{
var prefix = GetPrefix(candidate.PrimaryCategory);
var namePrefix = GetNamePrefix(candidate.PrimaryCategory);
var nextIndex = 1;
var existingIds = new HashSet<string>(existingRules.Select(rule => rule.Id ?? string.Empty), StringComparer.OrdinalIgnoreCase);
var existingNames = new HashSet<string>(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; }
}
}
}

View File

@@ -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<string> 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<string>();
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;
}
}
}

View File

@@ -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<string>(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<string> 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<string> 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<string> 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<string> 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<string> RemovedFiles { get; } = new List<string>();
public IList<string> Failures { get; } = new List<string>();
}
}

View File

@@ -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<OpenXmlPartRootElement> GetRelevantRootElements(WordprocessingDocument document)
{
var result = new List<KeyValuePair<string, OpenXmlPartRootElement>>();
var partQueue = new Queue<OpenXmlPartContainer>();
partQueue.Enqueue(document.MainDocumentPart);
var visited = new HashSet<OpenXmlPart>();
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<string, OpenXmlPartRootElement>(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<W.DeletedRun>().Any())
{
return false;
}
var run = leaf.Ancestors<W.Run>().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
{
}
}
}
}