//using Microsoft.Office.Interop.Word; using System.Drawing; using UtilLib; using System.Collections.Generic; using System; using Newtonsoft.Json; using Microsoft.Office.Interop.Word; using Microsoft.Office.Tools.Word; using Bookmark = Microsoft.Office.Tools.Word.Bookmark; using Section = Microsoft.Office.Interop.Word.Section; using WdColor = Microsoft.Office.Interop.Word.WdColor; using NPOI.XSSF.UserModel; using System.Windows.Forms; using NPOI.SS.UserModel; using System.IO; using AIProofread.Model; using NPOI.SS.Util; using NPOI.HSSF.Util; using AIProofread.core; using log4net; namespace AIProofread { public class DocumentUtil { static int markId = 0; public static readonly ILog Logger = LogHelper.GetLogger(typeof(DocumentUtil)); /// /// 添加一个书签 /// /// /// /// /// public static Bookmark AddBookmark(string color, int start, int end) { var doc = Globals.ThisAddIn.Application.ActiveDocument; var maxOffset = doc.Range().End; if (start > maxOffset || end > maxOffset) { return null; } var document = Globals.Factory.GetVstoObject(doc); var r = document.Range(start, end); var mark = document.Controls.AddBookmark(r, string.Format("ai_mark_{0}", markId++)); mark.Tag = "ai_proofread"; if (color != null) { // 给选区添加背景颜色 r.Shading.BackgroundPatternColor = (WdColor)ColorTranslator.ToOle(Colors.FromHex(color)); } return mark; } public static void SectionAddMark(string color) { var document = Globals.Factory.GetVstoObject(Globals.ThisAddIn.Application.ActiveDocument); // 获取当前文档对象 var doc = Globals.ThisAddIn.Application.ActiveDocument; // 获取选中 var sections = document.Sections; foreach (Section section in sections) { var r = section.Range; //Bookmark mark = r.Bookmarks.Add(); var mark = document.Controls.AddBookmark(r, string.Format("ai_mark_{0}", markId++)); mark.Tag = "ai_proofread"; // 给选区添加背景颜色 r.Shading.BackgroundPatternColor = (WdColor)ColorTranslator.ToOle(Colors.FromHex(color)); } } public static System.Collections.Generic.List GetAllBookmark() { var bookmarks = Globals.ThisAddIn.Application.ActiveDocument.Bookmarks; System.Collections.Generic.List list = new System.Collections.Generic.List(); foreach (Microsoft.Office.Interop.Word.Bookmark mark in bookmarks) { list.Add(mark.Name); } return list; } /// /// 删除所有标签 /// public static void RemoveBookmark() { RemoveBookmark(null); } public static void ClearProofreadMarks() { var document = Globals.ThisAddIn.Application.ActiveDocument; var bookmarks = document.Bookmarks; ControlCollection controls = Globals.Factory.GetVstoObject(document).Controls; foreach (Microsoft.Office.Interop.Word.Bookmark mark in bookmarks) { if (Config.IsProofreadMark(mark.Name)) { // 去除高亮 mark.Range.Shading.BackgroundPatternColor = WdColor.wdColorAutomatic; // 去除下划线 //mark.Range.Underline = WdUnderline.wdUnderlineNone; try { // feat(20250305): 清除批注的时候,如果监测到书签位置是空格,把空格给删了吧(无缓冲记录时) string text = mark.Range.Text; if (text != null && text.Trim().Length == 0) { mark.Range.Text = ""; } mark.Delete(); } catch (Exception e) { Logger.Error("remove mark" + e.Message, e); } } } } /// /// 删除标签 /// /// public static void RemoveBookmark(string markId) { var document = Globals.Factory.GetVstoObject(Globals.ThisAddIn.Application.ActiveDocument); if (document.Bookmarks.Count == 0) return; // 不存在要移除的标签 if (null != markId && !document.Bookmarks.Exists(markId)) return; var bookmarks = Globals.ThisAddIn.Application.ActiveDocument.Bookmarks; foreach (Microsoft.Office.Interop.Word.Bookmark mark in bookmarks) { if (mark.Name == markId) { mark.Range.Shading.BackgroundPatternColor = (WdColor)WdColorIndex.wdAuto; mark.Delete(); } } } public static System.Collections.Generic.List GetSectionText() { var document = Globals.Factory.GetVstoObject(Globals.ThisAddIn.Application.ActiveDocument); System.Collections.Generic.List list = new System.Collections.Generic.List(); if (document.Sections.Count == 0) return list; foreach (Section item in document.Sections) { list.Add(item.Range.Text); } return list; } /// /// 查找偏移量 /// private static readonly int INSERT_FIND_OFFSET = 5; /// /// 查找校对项对应的range /// /// 校对项 /// 当前校对项所在句子 /// 上一个校对项结束位置 /// /// /// public static Range FindRange(CorrectItem correct, CorrectContext sentense, ref int prevOffset, Microsoft.Office.Interop.Word.Document document, Range range) { var paragraphText = range.Text; string findText = correct.Origin; int wordStart = correct.Start; int wordEnd = correct.End; int offset = sentense.SentenceOffset; string originText = sentense.Insert; object Start = range.Start + offset + wordStart; object End = range.Start + offset + wordEnd; try { var activeDocument = document; // 查找对象位置 End = range.Start + offset + wordEnd; Start = range.Start + offset + wordStart; // 直接找到 var findRange = activeDocument.Range(ref Start, ref End); // 判断对应选区是否是要找的文本 if (findRange.Text == findText) { return findRange; } #region 使用前后关联进行强匹配 // 找前缀 var prefix = wordStart > 2 ? ( wordStart > INSERT_FIND_OFFSET ? originText.Substring(wordStart - INSERT_FIND_OFFSET, INSERT_FIND_OFFSET) : originText.Substring(0, wordStart) ) : null; // 找后缀 var suffix = prefix == null ? ( wordEnd + INSERT_FIND_OFFSET < originText.Length ? originText.Substring(wordStart, INSERT_FIND_OFFSET) : originText.Substring(wordStart, originText.Length - wordStart) ) : null; var start = prefix != null || suffix != null ? paragraphText.IndexOf(prefix ?? suffix, prevOffset) // item.start + : -1; if (start != -1) { var findOffset = range.Start + start + (prefix != null ? prefix.Length : 0); findRange = document.Range(findOffset, findOffset + wordEnd - wordStart); if (findRange.Text == findText) { return findRange; } } #endregion // 使用段落字符串进行强匹配 start = paragraphText.IndexOf(findText, prevOffset); if (start != -1) { start += range.Start; return document.Range(start, start + findText.Length); } var msg1 = new Dictionary{ {"message",range.Find.Found?"搜索到可用位置":"没有搜索到可用位置" }, { "search_start",range.Start }, { "search_end",range.End }, { "search_text",range.Text } }; Logger.Debug(JsonConvert.SerializeObject(msg1)); } catch (Exception ex) { Logger.Error(ex); } return null; } public static Bookmark FindBookMarkByCorrect(CorrectItem correct) { try { var document = Globals.ThisAddIn.ActiveDocument.CurrentDocument; var marks = document.Bookmarks; var markName = Config.BuildBookmarkName(correct.Id); if (!document.Bookmarks.Exists(markName)) return null; ControlCollection controls = Globals.Factory.GetVstoObject(document).Controls; //return controls[markName] as Bookmark; //var obj = controls[markName]; var bookmark = marks[markName]; var start = bookmark.Range.Start; var end = bookmark.Range.End; // 删除原有书签 controls.Remove(markName); return controls.AddBookmark(document.Range(start, end), markName); } catch (Exception ex) { Logger.Error(ex); } return null; } public static Bookmark FindRangeAndCreateBookmark(CorrectItem correct, CorrectContext sentense, Microsoft.Office.Interop.Word.Document document, ref int prevOffset) { Bookmark bookmark = null; try { ControlCollection controls = Globals.Factory.GetVstoObject(document).Controls; var markName = Config.BuildBookmarkName(correct.Id); // 判断是否已经存在 if (controls.Contains(markName)) { try { controls.Remove(markName); } catch (Exception) { } } // 判断段落是否存在 if (sentense.ParagraphNumber > document.Paragraphs.Count) return null; var paragraph = document.Paragraphs[sentense.ParagraphNumber]; var findRange = FindRangeByCorrect(sentense, correct, paragraph, document, prevOffset); if (findRange != null) { // 更新查找的结束位置 //prevOffset = findRange.End - paragraphStart; bookmark = controls.AddBookmark(findRange, markName); bookmark.Tag = "ai_proofread"; } } catch (Exception ex) { Logger.Error("create mark error:" + ex.Message + "\n" + ex.StackTrace + "\n\n"); } return bookmark; } private static Range FindBySentence(Range paraRange, CorrectContext c, CorrectItem item, Microsoft.Office.Interop.Word.Document document) { try { var allSentenceCount = paraRange.Sentences.Count; if (allSentenceCount < c.SentenceNumber) return null; var sentence = paraRange.Sentences[c.SentenceNumber]; //paraText.Substring(c.SentenceOffset, c.InsertLength); c.SentenceOffset = sentence.Start; var offset = c.SentenceOffset; if (c.Insert.TrimEnd() == sentence.Text.TrimEnd()) { if (item.Tag == "i") { return document.Range(offset + item.Start, offset + item.Start); } //int count = 0; var range = document.Range(offset + item.Start, offset + item.End + 1); #region 删除批注 //while (true && count++ < 10) //{ // // // try // { // if (range.Text == null && range.Comments.Count > 0) // { // // 删除批注 // foreach (Comment comment in range.Comments) // { // comment.Delete(); // } // continue; // } // else // { // break; // } // } // catch (Exception dce) // { // Logger.Log(dce); // } //} #endregion // 比对原始内容与校对原文是否一致 if (range.Text == item.Origin) { return range; } } // 直接找 var range1 = document.Range(offset + item.Start, offset + item.End + 1); // 兼容空格的全角与半角 if (item.Origin == " " && (range1.Text == " " || range1.Text == " " || range1.Text.Trim().Length == 0)) { return range1; } // if (range1.Text == item.Origin) { return range1; } return null; // 执行查找 //return FindTextInRange(sentence, item.Origin); } catch (Exception e) { Logger.Error("find by sentence error",e); } return null; } public static Range FindTextInRange(Range range, string searchText) { try { Find find = range.Find; if (range.Comments.Count > 0) { // 当前区域有批注 执行忽略模式 find.MatchPhrase = true; } // 设置查找条件 find.Text = searchText; find.Forward = true; find.Wrap = WdFindWrap.wdFindContinue; find.Execute(); if (find.Found) return range; } catch (Exception) { } return null; } private static Range FindRangeByCorrect(CorrectContext c, CorrectItem item, Paragraph paragraph, Microsoft.Office.Interop.Word.Document document, int prevOffset) { var originText = c.Insert; var paraRange = paragraph.Range; var paraText = paraRange.Text; var paraStart = paraRange.Start; // 定位句子的其实位置 //var offset = paraStart + c.SentenceOffset; ////var cutLength = Math.Min(c.InsertLen, paraText.Length - offset); var originFindRange = FindBySentence(paraRange, c, item, document); if (originFindRange != null) return originFindRange; // 如果是新增 则查找定位 if (item.Tag == "i") { // 找前缀 var prefix1 = item.Start > 2 ? ( item.Start > INSERT_FIND_OFFSET ? originText.Substring(item.Start - INSERT_FIND_OFFSET, INSERT_FIND_OFFSET) : originText.Substring(0, item.Start) ) : null; // 找后缀 var suffix1 = prefix1 == null ? ( item.End + INSERT_FIND_OFFSET < originText.Length ? originText.Substring(item.Start, INSERT_FIND_OFFSET) : originText.Substring(item.Start, originText.Length - item.Start) ) : null; // 偏移量 var start1 = prefix1 != null || suffix1 != null ? paraText.IndexOf(prefix1 ?? suffix1, prevOffset) : -1; if (start1 != -1) { var findOffset = paraStart + start1 + (prefix1 != null ? prefix1.Length : 0); return document.Range(findOffset, findOffset); } } if (prevOffset >= paraText.Length) { // 查找位置已经超过了整段长度了 Logger.Debug("prevOffset:" + prevOffset + " paraText.Length:" + paraText.Length); return null; } // 执行查找 int wordStart = item.Start; int wordEnd = item.End; // 找前缀 var prefix = wordStart > 2 ? ( wordStart > INSERT_FIND_OFFSET ? originText.Substring(wordStart - INSERT_FIND_OFFSET, INSERT_FIND_OFFSET) : originText.Substring(0, wordStart) ) : null; // 找后缀 var suffix = prefix == null ? ( wordEnd + INSERT_FIND_OFFSET < originText.Length ? originText.Substring(wordStart, INSERT_FIND_OFFSET) : originText.Substring(wordStart, originText.Length - wordStart) ) : null; var start = prefix != null || suffix != null ? paraText.IndexOf(prefix ?? suffix, prevOffset) // item.start + : -1; if (start != -1) { var findOffset = paraRange.Start + start + (prefix != null ? prefix.Length : 0); prevOffset = start; var range = document.Range(findOffset, findOffset + wordEnd - wordStart + 1); if (item.Origin == " " && (range.Text == " " || range.Text == " " || range.Text.Trim().Length == 0)) { return range; } if (range.Text == item.Origin) { return range; } } // 使用find查找 var r = FindTextInRange(paraRange, item.Origin); if (r != null) { // 判断找到的range是否和查找区域误差过大 return r; } // 直接定位查找 start = paraText.IndexOf(item.Origin, prevOffset); if (start == -1) return null; // 定位整体开始位置 start = paraStart + start; return document.Range(start, start + item.Origin.Length); } public static void ExportProofreadResult(string modelType) { string currentName = Globals.ThisAddIn.Application.ActiveDocument.Name; // 去掉文件名后缀 currentName = currentName.Substring(0, currentName.LastIndexOf(".")); SaveFileDialog sfd = new SaveFileDialog(); modelType = modelType == "full" ? "查全" : "查准"; // 设置默认文件名 sfd.FileName = currentName + $"_勘误表_优先{modelType}.xlsx"; sfd.Filter = "Excel文件|*.xlsx"; var result = sfd.ShowDialog(); // 如果用户取消选择,则返回 if (result == DialogResult.Cancel) { return; } try { if (File.Exists(sfd.FileName)) { // 判断原始文件是否可以删除 if (File.GetAttributes(sfd.FileName).HasFlag(FileAttributes.ReadOnly)) { Globals.ThisAddIn.ShowDialog("已经存在名勘误表文件,请更换名称或手动删除", null, null); return; } // 删除文件 重新写入新数据避免出现未知不可控bug File.Delete(sfd.FileName); } //ProcessExport(sfd.FileName); CorrectResultExportor.GetInstance().ExportResult(sfd.FileName); Globals.ThisAddIn.ActiveDocument?.ShowMessage("导出勘误表成功", 2000, false); } catch (Exception e) { Logger.Error("导出勘误表失败",e); Globals.ThisAddIn.ShowDialog("导出勘误表失败,请重试", null, null); } } private static NPOI.SS.UserModel.ICell CreateCell(IRow row, int colIndex, IFont font, string text, ICellStyle style = null) { var cell = row.CreateCell(colIndex); if (font != null) { var value = new XSSFRichTextString(text); value.ApplyFont(font); cell.SetCellValue(value); } else { cell.SetCellValue(text); } if (style != null) { cell.CellStyle = style; } else { cell.CellStyle.WrapText = true; } return cell; } private static int RGB(int red, int green, int blue) { return red + (green << 8) + (blue << 16); } private static void ProcessExport(string fileName) { using (var fs = File.Create(fileName)) { var book = new XSSFWorkbook(); var sheet = book.CreateSheet("Sheet1"); #region 基础设置 var simHeiFont = CreateBaseFont(book, "黑体", 11); simHeiFont.Color = NPOI.HSSF.Util.HSSFColor.Black.Index; // 设置表格样式 var style = CreateBaseCellStyle(book); style.FillPattern = FillPattern.NoFill; // SolidForeground /* 系统与正文用不同字体区分(包括顶部栏、处理列、新增、删除等提示)勘误表系统字体为黑体,所有字号统一 */ style.SetFont(simHeiFont); style.BorderBottom = NPOI.SS.UserModel.BorderStyle.None; style.BorderTop = NPOI.SS.UserModel.BorderStyle.None; style.BorderLeft = NPOI.SS.UserModel.BorderStyle.None; style.BorderRight = NPOI.SS.UserModel.BorderStyle.None; style.WrapText = true; // 设置宽度 sheet.SetColumnWidth(0, 1); // 序号 sheet.SetColumnHidden(0, true); // 隐藏序号 sheet.SetColumnWidth(3, 80 * 256); // 详细信息 sheet.SetColumnWidth(4, 20 * 256); // 异常 sheet.SetColumnWidth(5, 20 * 256); // 建议 sheet.SetColumnWidth(6, 10 * 256); // 处理状态 string[] headerTitles = { "序号","页","行","详细信息","异常", "建议","处理状态" }; // 设置表头筛选及冻结 2.0.5 2024-12-30修改 sheet.CreateFreezePane(0, 1, 0, 1); sheet.SetAutoFilter(CellRangeAddress.ValueOf("A1:G1")); // 设置表头为白色 var headerStyle = book.CreateCellStyle(); var headerFont = book.CreateFont(); headerFont.FontName = "黑体"; // 设置字体 headerFont.FontHeightInPoints = 11; // 字体大小 var themeColor = new XSSFColor(new byte[] { 214, 170, 105 }); // 首行背景色黑色,字体用主题金色 ((XSSFFont)headerFont).SetColor(themeColor); headerStyle.SetFont(headerFont); // 应用字体 // 设置背景颜色为黑色 headerStyle.FillForegroundColor = HSSFColor.Black.Index; // 设置前景色 headerStyle.FillPattern = FillPattern.SolidForeground; // 使用实心填充模式 // 对齐方式 headerStyle.VerticalAlignment = VerticalAlignment.Center; // 垂直居中 headerStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center; // 水平居中 // 表头设置 var row = sheet.CreateRow(0); row.Height = 40 * 20; // 首行加高 for (int i = 0; i < 7; i++) { sheet.SetDefaultColumnStyle(i, style); var cell = CreateCell(row, i, headerFont, headerTitles[i], headerStyle); } var blackFont = CreateBaseFont(book, NPOI.HSSF.Util.HSSFColor.Black.Index); var redFont = CreateBaseFont(book, NPOI.HSSF.Util.HSSFColor.Red.Index); // 获取排序后的数据 var list = ExportDataItem.GetExportData(Globals.ThisAddIn.ActiveDocument.marks); // 对 list 进行排序 int id = 1; var wrapTextStyle = book.CreateCellStyle(); wrapTextStyle.WrapText = true; wrapTextStyle.VerticalAlignment = VerticalAlignment.Center; var alignCenterStyle = book.CreateCellStyle(); alignCenterStyle.VerticalAlignment = VerticalAlignment.Center; alignCenterStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center; #endregion foreach (var item in list) { try { var it = item.Item; row = sheet.CreateRow(id); row.Height = -1; row.CreateCell(0).SetCellValue(id); // 页码 var pageCell = row.CreateCell(1); pageCell.CellStyle = alignCenterStyle; pageCell.SetCellValue(item.PageNumber); // 行号 var lineCell = row.CreateCell(2); lineCell.CellStyle = alignCenterStyle; lineCell.SetCellValue(item.LineNumber); #region 引用原文内容 string originSentence = item.OriginSentence.TrimEnd(); var startIndex = it.Tag == "i" || it.Start < originSentence.Length - 1 ? it.Start : originSentence.IndexOf(it.Origin); XSSFRichTextString originText = new XSSFRichTextString(originSentence); originText.ApplyFont(blackFont); try { originText.ApplyFont(it.Start, it.Start + (it.Tag == "i" ? it.Text.Length : it.Origin.Length), redFont); } catch (Exception e) { Logger.Error(e); } var oriCell = row.CreateCell(3); // 设置单元格内容自动换行 oriCell.CellStyle = wrapTextStyle; oriCell.SetCellValue(originText); #endregion // 原始内容 var cellOrigin = row.CreateCell(4); cellOrigin.CellStyle = wrapTextStyle; cellOrigin.SetCellValue(it.Origin); #region 建议 var suggest = it.Text; var cellSuggest = row.CreateCell(5); cellSuggest.CellStyle = wrapTextStyle; if (it.Tag == "r") { string tag = ""; if (it.Type == "blacklist") { tag = "黑名单"; } else { // TODO 后期优化 // 易错词 标点 不标识 if (it.Type == "confusion" || it.Type == "model" || it.Type == "other" || it.Type == "punctuation") { //row.CreateCell(5).SetCellValue(suggest); } else if (!string.IsNullOrEmpty(it.Type) && ExportConfig.ErrorTypeMap.ContainsKey(it.Type.ToLower())) { tag = $"【提示】{ExportConfig.ErrorTypeMap[it.Type.ToLower()]}"; } else if (!string.IsNullOrEmpty(it.Addition) && it.Addition.Trim().Length > 0 && it.Addition != "提示") { tag = $"【提示】{it.Addition}"; } } if (string.IsNullOrEmpty(tag)) { cellSuggest.SetCellValue(suggest); } else { XSSFRichTextString replaceText = new XSSFRichTextString(suggest + $" {tag}"); replaceText.ApplyFont(blackFont); // 对查找内容引用红色 replaceText.ApplyFont(suggest.Length + 1, suggest.Length + 1 + tag.Length, simHeiFont); cellSuggest.SetCellValue(replaceText); } } else { string tag = ""; startIndex = 0; if (it.Type == "incorrect_expression") { tag = "表述有误"; } else if (it.Type == "sensitive") { tag = "敏感词"; } else if (it.Type == "blacklist" || it.Type == "blacklist_mask") { tag = "黑名单"; } else if (it.Type == "fallen_officers") { tag = "落马官员"; } else if (it.Tag == "i") { startIndex = it.Text.Length + 1; tag = it.Text + " 新增"; } else if (it.Tag == "d") { tag = "删除"; } XSSFRichTextString suggestText = new XSSFRichTextString(tag); suggestText.ApplyFont(blackFont); if (tag.Length > 0) { // 对查找内容引用红色 suggestText.ApplyFont(startIndex, it.Tag == "i" ? startIndex + 2 : tag.Length, simHeiFont); cellSuggest.SetCellValue(suggestText); } else { cellSuggest.SetCellValue(suggest); } } #endregion // 处理状态 var statusValue = new XSSFRichTextString(StatusText(it.IsAccept)); statusValue.ApplyFont(simHeiFont); var cellStatus = row.CreateCell(6); cellStatus.CellStyle = alignCenterStyle; cellStatus.SetCellValue(statusValue); } catch (Exception ex) { Logger.Error(ex); } id++; } // 保存到文件 book.Write(fs); //Globals.ThisAddIn.ShowMessage("导出成功", 3000); } } private static ICellStyle CreateBaseCellStyle(IWorkbook workbook) { var style = workbook.CreateCellStyle(); style.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center; style.VerticalAlignment = NPOI.SS.UserModel.VerticalAlignment.Center; style.WrapText = true; return style; } private static IFont CreateBaseFont(IWorkbook workbook, string name, int size) { // 宋体 11 var font = workbook.CreateFont(); font.FontName = name; font.FontHeightInPoints = size; return font; } private static IFont CreateBaseFont(IWorkbook workbook, short color = -1) { var font = CreateBaseFont(workbook, "宋体", 11); if (color != -1) { font.Color = color; } return font; } private static string StatusText(int status) { if (status == AcceptStatus.Accept) return "已采纳"; else if (status == AcceptStatus.Review) return "已复核"; else if (status == AcceptStatus.Ignore) return "已忽略"; return "未处理"; } private static string GetInsertContentByLength(int length) { return new String('˽', length); } } }