using System; using System.Collections.Generic; using System.Xml; using iTextSharp.text; using iTextSharp.text.pdf; using iTextSharp.text.pdf.parser; using PDFPatcher.Common; using PDFPatcher.Model; namespace PDFPatcher.Processor { sealed class AutoBookmarkCreator { sealed class SizeOccurrence(float size, int page, string instance) { public float Size { get; set; } = size; public int FirstPage { get; set; } = page; public string FirstInstance { get; set; } = instance.Length > 50 ? instance.Substring(0, 50) : instance; public int Occurrence { get; set; } = 1; } sealed class FontOccurrence { readonly Dictionary> oc = new Dictionary>(); internal List GetOccurrence(string fontName) { return oc.TryGetValue(fontName, out List s) ? s : null; } internal void AddOccurrence(string fontName, float size, int page, string instance) { if (oc.ContainsKey(fontName) == false) { oc.Add(fontName, new List() { new SizeOccurrence(size, page, instance) }); } else { var o = oc[fontName].Find((s) => s.Size == size); if (o != null) { o.Occurrence++; } else { oc[fontName].Add(new SizeOccurrence(size, page, instance)); } } } } const string __AddSpaceAfterCharacters = ":.,\"'?!)]};"; const string __InsertSpaceBeforeCharacters = "\"'([{"; readonly PdfReader _reader; readonly AutoBookmarkOptions _options; const int OpenWorkload = 10; public AutoBookmarkCreator(PdfReader reader, AutoBookmarkOptions options) { _reader = reader; _options = options; TextLine.DefaultDirection = options.WritingDirection; } internal int EstimateWorkload() { var n = _reader.NumberOfPages; var load = 0; load += OpenWorkload; var t = PageRangeCollection.Parse(_options.PageRanges, 1, n, true).TotalPages; load += t > 0 ? t : n; return load; } internal void ExportAutoBookmarks(XmlWriter w, AutoBookmarkOptions options) { AutoCreateBookmarks(w, _reader, options); } internal void AutoCreateBookmarks(XmlWriter writer, PdfReader reader, AutoBookmarkOptions options) { Tracker.IncrementProgress(OpenWorkload); int pn = reader.NumberOfPages + 1; var c = new AutoBookmarkContext() { TotalPageNumber = reader.NumberOfPages }; var p = new TextToBookmarkProcessor(options, c); var lp = new LevelProcessor(options.LevelAdjustment); var ranges = PageRangeCollection.Parse(options.PageRanges, 1, reader.NumberOfPages, true); var doc = new XmlDocument(); var be = doc.AppendChild(doc.CreateElement(Constants.Bookmark)) as XmlElement; float size = -1; var sizes = new Stack(); float yOffset = 1 + options.YOffset; int level = 0; const string indentString = "          "; List ig; var fontOccurrences = new FontOccurrence(); if (options.IgnorePatterns.Count == 0) { ig = null; } else { ig = new List(); foreach (var item in options.IgnorePatterns) { if (String.IsNullOrEmpty(item.Text)) { continue; } try { ig.Add(item.CreateMatcher()); } catch (ArgumentException ex) { Tracker.TraceMessage(Tracker.Category.Alert, (String.Concat("忽略文本(", item, ")无效:", ex.Message))); } } } XmlWriter oldWriter = null; if (options.ExportTextCoordinates == false) { oldWriter = writer; writer = new NullXmlWriter(); } foreach (PageRange r in ranges) { for (int i = r.StartValue; i <= r.EndValue && i < pn; i++) { if (i == 1 && options.CreateBookmarkForFirstPage) { continue; } //Tracker.TraceMessage (String.Concat ("分析第 ", i, " 页。")); var box = reader.GetCropBox(i); p.Reset(); c.PageBox = box; c.CurrentPageNumber = i; var pr = reader.GetPageRotation(i); pr = PdfHelper.NormalizeRotationNumber(pr); if (pr != 0) { p.RotationMatrix = pr switch { 90 => new Matrix(0, 1, -1, 0, 0, 0), 180 => new Matrix(0, -1, -1, 0, 0, 0), 270 => new Matrix(0, -1, 1, 0, 0, 0), _ => p.RotationMatrix }; } p.ProcessContent(reader.GetPageContent(i), reader.GetPageNRelease(i).GetAsDict(PdfName.RESOURCES)); c.IsTextMerged = false; c.TextLine = null; // TODO: 自动根据已知排版方向比例修正排版方向 // 合并前筛选文本 var ptl = p.TextList; for (int li = ptl.Count - 1; li >= 0; li--) { c.TextInfo = ptl[li]; if (lp.ChangeSizeLevel(c) < options.TitleThreshold) { ptl.RemoveAt(li); } } var tl = MergeTextInfoList(box, ptl, _options); // TODO: 筛选文本 c.IsTextMerged = true; for (int li = tl.Count - 1; li >= 0; li--) { c.TextLine = tl[li]; c.TextInfo = c.TextLine.FirstText; if ((c.TextInfo.Size = lp.ChangeSizeLevel(c)) < options.TitleThreshold) { tl.RemoveAt(li); } } var tr = MergeTextLines(box, tl); if (tr != null && tr.Count > 0) { if (options.WritingDirection != WritingDirection.Unknown) { tr.Sort((a, b) => { var ra = a.Region; var rb = b.Region; if (ra.Middle < rb.Middle) return 1; else if (ra.Middle > rb.Middle) return -1; else if (ra.Center > rb.Center) return 1; else if (ra.Center < rb.Center) return -1; else return 0; }); } writer.WriteStartElement(Constants.Content.Texts); writer.WriteValue(Constants.Content.PageNumber, i); foreach (var item in tr) { var t = PdfHelper.GetValidXmlString(ConcatRegionText(item)).Trim(); var f = item.Lines[0].FirstText.Font; var s = item.Lines[0].FirstText.Size; writer.WriteStartElement("文本"); writer.WriteAttributeString(Constants.Font.ThisName, f != null ? f.FontID.ToText() : "OCR"); writer.WriteValue(Constants.Coordinates.Top, item.Region.Top); writer.WriteValue(Constants.Coordinates.Bottom, item.Region.Bottom); writer.WriteValue(Constants.Coordinates.Left, item.Region.Left); writer.WriteValue(Constants.Coordinates.Width, item.Region.Width); writer.WriteValue(Constants.Coordinates.Height, item.Region.Height); writer.WriteValue("尺寸", s); writer.WriteString(t); writer.WriteEndElement(); if (t.Length == 0 || (t.Length == 1 && options.IgnoreSingleCharacterTitle) || (options.IgnoreNumericTitle && AutoBookmarkOptions.NumericPattern.IsMatch(t)) ) { continue; } if (ig != null) { bool ignore = false; foreach (var rg in ig) { if (rg.Matches(t)) { ignore = true; continue; } } if (ignore) { continue; } } if (options.AutoHierarchicalArrangement) { do { if (sizes.Count == 0 || s < (size = sizes.Peek())) { be = be.AppendChild(doc.CreateElement(Constants.Bookmark)) as XmlElement; sizes.Push(s); level++; break; } else if (s == size) { be = (be.ParentNode ?? be).AppendChild(doc.CreateElement(Constants.Bookmark)) as XmlElement; break; } be = be.ParentNode as XmlElement; sizes.Pop(); level--; } while (s > size && be.NodeType == XmlNodeType.Element); } else { be = doc.DocumentElement.AppendChild(doc.CreateElement(Constants.Bookmark)) as XmlElement; } be.SetAttribute(Constants.BookmarkAttributes.Title, t); be.SetAttribute(Constants.DestinationAttributes.Page, i.ToText()); be.SetAttribute(Constants.DestinationAttributes.View, Constants.DestinationAttributes.ViewType.XYZ); be.SetAttribute(Constants.Coordinates.Top, ValueHelper.ToText(item.Region.Top + s * yOffset)); be.SetAttribute(Constants.Font.Size, s.ToText()); if (f != null) { be.SetAttribute(Constants.Font.ThisName, f.FontID.ToText()); } CountFontOccurrenceInRegion(fontOccurrences, i, item); #if DEBUG Tracker.TraceMessage(String.Concat(item.Direction.ToString()[0], ':', level < 11 ? indentString.Substring(0, level) : indentString, t, " .... ", i.ToText())); #else Tracker.TraceMessage (String.Concat (level < 11 ? indentString.Substring (0, level) : indentString, t, " .... ", ValueHelper.ToText (i))); #endif } writer.WriteEndElement(); } Tracker.IncrementProgress(1); } } if (oldWriter != null) { writer = oldWriter; } WriteDocumentFontOccurrences(writer, options, p, fontOccurrences); SetGoToTop(options, doc); writer.WriteStartElement(Constants.DocumentBookmark); if (options.CreateBookmarkForFirstPage && String.IsNullOrEmpty(options.FirstPageTitle) == false) { writer.WriteStartElement(Constants.Bookmark); writer.WriteAttributeString(Constants.BookmarkAttributes.Title, options.FirstPageTitle); writer.WriteAttributeString(Constants.DestinationAttributes.Page, "1"); writer.WriteAttributeString(Constants.DestinationAttributes.Action, Constants.ActionType.Goto); writer.WriteEndElement(); } doc.DocumentElement.WriteContentTo(writer); writer.WriteEndElement(); } static void SetGoToTop(AutoBookmarkOptions options, XmlDocument doc) { if (options.PageTopForLevel > 0) { var topics = doc.DocumentElement.SelectNodes(".//书签[count(ancestor::书签) < " + (options.PageTopForLevel + 1) + "]"); foreach (XmlElement t in topics) { t.RemoveAttribute(Constants.Coordinates.Top); } } } static void WriteDocumentFontOccurrences(XmlWriter writer, AutoBookmarkOptions options, TextToBookmarkProcessor p, FontOccurrence fontOccurrences) { writer.WriteStartElement(Constants.Font.DocumentFont); Tracker.TraceMessage("\n文档所用的字体"); var dl = new List(); foreach (var item in p.FontList) { var fo = "0"; var sl = fontOccurrences.GetOccurrence(item.Value); if (sl != null) { if (dl.Contains(item.Value) == false) { int o = 0; foreach (var s in sl) { o += s.Occurrence; } fo = o.ToText(); dl.Add(item.Value); } else { sl = null; } } if (options.DisplayFontStatistics && (sl != null || options.DisplayAllFonts)) { Tracker.TraceMessage(String.Concat("编号:", item.Key, "\t出现次数:", fo, "\t名称:", item.Value)); } writer.WriteStartElement(Constants.Font.ThisName); writer.WriteAttributeString(Constants.Font.ID, item.Key.ToText()); writer.WriteAttributeString(Constants.Font.Name, item.Value); writer.WriteAttributeString(Constants.FontOccurrence.Count, fo); if (sl != null) { foreach (var s in sl) { writer.WriteStartElement(Constants.Font.Size); writer.WriteAttributeString(Constants.Font.Size, s.Size.ToText()); writer.WriteAttributeString(Constants.FontOccurrence.Count, s.Occurrence.ToText()); writer.WriteAttributeString(Constants.FontOccurrence.FirstText, s.FirstInstance); writer.WriteAttributeString(Constants.FontOccurrence.FirstPage, s.FirstPage.ToText()); if (options.DisplayFontStatistics && (s.Occurrence > 0 || options.DisplayAllFonts)) { Tracker.TraceMessage($"\t尺寸:{s.Size.ToText()}\t出现次数:{s.Occurrence.ToText()}\t首次出现于第{s.FirstPage.ToText()}页({s.FirstInstance})"); } writer.WriteEndElement(); } } writer.WriteEndElement(); } writer.WriteEndElement(); } static void CountFontOccurrenceInRegion(FontOccurrence fontOccurrences, int i, TextRegion item) { FontInfo f = null; foreach (var il in item.Lines) { foreach (var ii in il.Texts) { if (ii.Font != null && (f == null || ii.Font.FontID != f.FontID)) { fontOccurrences.AddOccurrence(ii.Font.FontName, ii.Size, i, il.Text); f = ii.Font; } } } } static string ConcatRegionText(TextRegion region) { var ls = region.Lines; if (ls.Count == 0) { return String.Empty; } else if (ls.Count == 1) { return ls[0].Text; } ls = new List(ls); if (region.Direction == WritingDirection.Vertical) { ls.Sort((a, b) => { if (a.Region.Middle < b.Region.Middle) { return 1; } else if (a.Region.Middle > b.Region.Middle) { return -1; } return 0; }); } var sb = StringBuilderCache.Acquire(); sb.Append(ls[0].Text); for (int i = 1; i < ls.Count; i++) { var l = ls[i].Text; var ll = ls[i - 1].Text; if (ll.Length > 0 && l.Length > 0) { var c1 = l[l.Length - 1]; var c2 = ll[0]; if ((__AddSpaceAfterCharacters.IndexOf(c1) != -1 || (Char.IsLetterOrDigit(c1) && c1 < 0x4E00 /*非中文字符*/)) && (__InsertSpaceBeforeCharacters.IndexOf(c2) != -1 || (Char.IsLetterOrDigit(c2) && c2 < 0x4E00))) { sb.Append(' '); } } sb.Append(l); } return StringBuilderCache.GetStringAndRelease(sb); } /// /// 使用最小距离法将 的文本聚类为 列表。 /// /// 包含文本位置及尺寸信息的 集合。 /// 聚类后所得的 列表。 internal static List MergeTextInfoList(Rectangle pageBox, IList textInfos, AutoBookmarkOptions options) { var ll = new List(); // 同行合并宽度最小值 var cw = pageBox.Width / 6; var ch = pageBox.Height / 6; var dirCount = new int[4]; // 遍历识别所得的各 TextInfo,使用最小距离聚类方法将其聚类为行 foreach (var item in textInfos) { var ir = item.Region; DistanceInfo cd = null; // TextInfo 到 TextLine 的距离 var md = new DistanceInfo(DistanceInfo.Placement.Unknown, float.MaxValue, float.MaxValue); // 最小距离 TextLine ml = null; // 最小距离的 TextLine // 求最小距离的 TextLine float ds = item.Size / 10; // 循环只包含了 TextLine,未包含文本 TextInfo 的其余文本 var end = ll.Count > 5 ? ll.Count - 5 : 0; for (int i = ll.Count - 1; i >= end; i--) { var li = ll[i]; // 文本尺寸应在误差范围之内 if (Math.Abs(item.Size - li.FirstText.Size) > ds && options.MergeDifferentSizeTitles == false) { continue; } if (options.MergeDifferentFontTitles == false && li.FirstText.Font.FontID != item.Font.FontID) { break; } cd = li.GetDistance(ir); if ((cd.IsOverlapping // 当前项与文本行交叠 && (md.IsOverlapping == false // 最小距离不是交叠 || cd.DistanceRadial < md.DistanceRadial) // 当前项与文本行的交叠中心距离小于最小距离 ) //&& (options.MergeDifferentFontTitles || li.FirstText.Font.FontID == item.Font.FontID) || ((md.Location == DistanceInfo.Placement.Unknown // 未知最小距离 || (cd.IsOverlapping == false && md.IsOverlapping == false && cd.MinDistance < md.MinDistance) // 当前项与文本行的距离小于最小距离 ) && (((cd.IsHorizontallyAligned) // 相对位置为水平 && li.Direction != WritingDirection.Vertical // 文本行方向不为纵向 && item.Region.IsAlignedWith(li.Region, WritingDirection.Horizontal) // 两者处于同一横行 ) || ((cd.IsVerticallyAligned) // 相对位置为垂直 && li.Direction != WritingDirection.Horizontal // 文本行方向不为横向 && item.Region.IsAlignedWith(li.Region, WritingDirection.Vertical) // 两者处于同一纵行 // && Math.Abs (item.Region.Middle - li.Region.Middle) < li.Region.Height // 行间距离小于行高 ) ) && (options.DetectColumns == false || cd.MinDistance < cw) && (options.MergeDifferentFontTitles || li.FirstText.Font.FontID == item.Font.FontID) ) ) { md = cd; ml = li; if (cd.IsLeft) { dirCount[0]++; } else if (cd.IsRight) { dirCount[1]++; } else if (cd.IsAbove) { dirCount[2]++; } else if (cd.IsBelow) { dirCount[3]++; } } } // 否则,用 item 创建新的 TextLine if (item.Text.Length == 0) { item.Text = " "; } if (ml != null) { // 若存在最小距离的 TextLine 且可合并,则将 item 归入 TextLine if (md.IsOverlapping && options.IgnoreOverlappedText) { // 检查是否存在交叠重复的文本 foreach (var t in ml.Texts) { if (t.Region.IntersectWith(item.Region) // item 与 TextLine 中某项交叠 && (t.Text.Contains(item.Text) || item.Text.Contains(t.Text) // 交叠的项文本和 item 的文本相同 ) ) { goto Next; // 忽略此项目 } } } ml.AddText(item); } else { ll.Add(new TextLine(item)); } Next: continue; } return ll; } internal List MergeTextLines(Rectangle pageBox, IList textLines) { var ll = new List(); // 同行合并宽度最小值 var cw = pageBox.Width / 6; var ch = pageBox.Height / 6; // 遍历识别所得的各 TextInfo,使用最小距离聚类方法将其聚类为行 foreach (var item in textLines) { var ir = item.Region; DistanceInfo cd = null; // TextInfo 到 TextLine 的距离 var md = new DistanceInfo(DistanceInfo.Placement.Unknown, float.MaxValue, float.MaxValue); // 最小距离 TextRegion mr = null; // 最小距离的 TextRegion // 求最小距离的 TextLine float ds = item.FirstText.Size / 10; // 循环只包含了 TextLine,未包含文本 TextInfo 的其余文本 for (int i = ll.Count - 1; i >= 0; i--) { var li = ll[i]; // 文本尺寸应在误差范围之内 if (Math.Abs(item.FirstText.Size - li.Lines[0].FirstText.Size) > ds && _options.MergeAdjacentTitles) { continue; } if (_options.MergeDifferentFontTitles == false && li.Lines[0].FirstText.Font.FontID != item.FirstText.Font.FontID) { break; } cd = li.Region.GetDistance(ir, li.Direction); if ((cd.IsOverlapping // 当前项与文本行交叠 && (md.IsOverlapping == false // 最小距离不是交叠 || cd.DistanceRadial < md.DistanceRadial) // 当前项与文本行的交叠中心距离小于最小距离 ) || ((md.Location == DistanceInfo.Placement.Unknown // 未知最小距离 || (cd.IsOverlapping == false && md.IsOverlapping == false && cd.MinDistance < md.MinDistance) // 当前项与文本行的距离小于最小距离 ) && (((cd.IsHorizontallyAligned) // 相对位置为水平 && li.Direction != WritingDirection.Vertical // 文本行方向不为纵向 && item.Region.IsAlignedWith(li.Region, WritingDirection.Horizontal) // 两者处于同一横行 && cd.MinDistance < item.Region.Width * _options.MaxDistanceBetweenLines // 行间距离小于指定行宽 && _options.MergeAdjacentTitles && (_options.MergeDifferentSizeTitles || li.Lines[0].Region.Width == item.Region.Width) // 合并相同尺寸的标题 ) || ((cd.IsVerticallyAligned) // 相对位置为垂直 && li.Direction != WritingDirection.Horizontal // 文本行方向不为横向 && item.Region.IsAlignedWith(li.Region, WritingDirection.Vertical) // 两者处于同一纵行 && cd.MinDistance < item.Region.Height * _options.MaxDistanceBetweenLines // 行间距离小于指定行高 && _options.MergeAdjacentTitles && (_options.MergeDifferentSizeTitles || li.Lines[0].Region.Height == item.Region.Height) // 合并相同尺寸的标题 ) ) && cd.MinDistance < cw ) ) { md = cd; mr = li; } } // 否则,用 item 创建新的 TextLine if (mr != null) { // 若存在最小距离的 TextLine 且可合并,则将 item 归入 TextLine mr.AddLine(item); } else { ll.Add(new TextRegion(item)); } } return ll; } sealed class TextToBookmarkProcessor : PdfContentStreamProcessor { readonly float _fontSizeThreshold; //Rectangle _positionRectangle; readonly bool _mergeAdjacentTitles; readonly bool _mergeDifferentSizeTitles; float _textWidth, _charWidth; readonly List _TextList; readonly LevelProcessor _levelProcessor; readonly AutoBookmarkContext _context; public TextToBookmarkProcessor(AutoBookmarkOptions options, AutoBookmarkContext context) { _fontSizeThreshold = options.TitleThreshold; _mergeAdjacentTitles = options.MergeAdjacentTitles; _mergeDifferentSizeTitles = options.MergeDifferentSizeTitles; _levelProcessor = new LevelProcessor(options.LevelAdjustment); _TextList = new List(); PopulateOperators(); RegisterContentOperator("TJ", new AccumulatedShowTextArray()); _context = context; } /// /// 获取页面内容的文本。 /// internal List TextList => _TextList; /// /// 获取字体列表。 /// internal IDictionary FontList => Fonts; public Matrix RotationMatrix { get; set; } internal override void Reset() { base.Reset(); _TextList?.Clear(); } protected override void DisplayPdfString(PdfString str) { var gs = CurrentGraphicState; var font = gs.Font; var chars = font.DecodeText(str); float totalWidth = 0, charWidth = 0; foreach (var c in chars) { float w = font.GetWidth(c) / 1000.0f; if (w == 0 && (font.CjkType & FontInfo.CjkFontType.CJK) > 0) { w = c < 0xFF ? 0.5f : 1f; } float wordSpacing = c == ' ' ? gs.WordSpacing : 0f; if (Char.IsLetterOrDigit(c)) { charWidth += w * gs.FontSize * gs.HorizontalScaling; } totalWidth += (w * gs.FontSize + gs.CharacterSpacing + wordSpacing) * gs.HorizontalScaling; } _textWidth = totalWidth; _charWidth = charWidth; AdjustTextMatrixX(totalWidth); } protected override void InvokeOperator(PdfLiteral oper, List operands) { var o = oper.ToString(); string text; float size; Matrix tm; switch (o) { case "TJ": tm = GetTextMatrix(); size = GetFontSize(tm); text = DecodeTJText(operands, size); break; case "Tj": case "'": case "\"": tm = GetTextMatrix(); size = GetFontSize(tm); if (size < 0) { size = -size; } text = CurrentGraphicState.Font.DecodeText(operands[0] as PdfString); break; default: // 执行默认的操作指令 base.InvokeOperator(oper, operands); return; } // 处理文本 base.InvokeOperator(oper, operands); //if (tm[Matrix.I12] != 0 || tm[Matrix.I21] != 0) { // // 忽略非横向文本 // goto Exit; //} if (size < 0.0001) { size = 0.0001f; } else { size = (float)Math.Round(size, 4); } var ti = new TextInfo() { Text = text.Length > 1 ? text.TrimEnd(' ') : text, Size = size, Region = CreateBoundFromMatrix(tm, _textWidth, size), Font = CurrentGraphicState.Font, LetterWidth = _charWidth * tm[Matrix.I22] }; if (ti.LetterWidth < 0) { ti.LetterWidth = -ti.LetterWidth; } if (IsBoundOutOfRectangle(_context.PageBox, ti.Region)) { // 文本落在页面之外 goto Exit; } //TODO: 筛选文本 _TextList.Add(ti); Exit: return; } string DecodeTJText(List operands, float size) { var array = (PdfArray)operands[0]; float d = size * CurrentGraphicState.HorizontalScaling * 4f / 1000f; var t = new string[array.Size]; int i = 0; foreach (PdfObject item in array.ArrayList) { if (item.Type == PdfObject.STRING) { t[i++] = CurrentGraphicState.Font.DecodeText(item as PdfString); } else if (item.Type == PdfObject.NUMBER) { if (-(item as PdfNumber).FloatValue * d > size) { t[i++] = " "; } } } return String.Concat(t); } float GetFontSize(Matrix tm) { float size = CurrentGraphicState.FontSize * tm[Matrix.I22]; if (size < 0) { size = -size; } return size; } Matrix GetTextMatrix() { return RotationMatrix != null ? RotationMatrix.Multiply(TextMatrix).Multiply(CurrentGraphicState.TransMatrix) : TextMatrix.Multiply(CurrentGraphicState.TransMatrix); } static Bound CreateBoundFromMatrix(Matrix tm, float textWidth, float size) { var l = tm[Matrix.I31]; var b = tm[Matrix.I32]; var r = tm[Matrix.I31] + textWidth * tm[Matrix.I11]; var t = tm[Matrix.I32] + size; float x; if (l > r) { x = r; r = l; l = x; } if (b > t) { x = t; t = b; b = x; } return new Bound(l, b, r, t); } /// /// 检查 是否处于 之内。 /// /// 大边框。 /// 小边框。 /// 小边框完全处于大边框内,则返回 true。 internal static bool IsBoundOutOfRectangle(Rectangle a, Bound b) { return b.Right < a.Left || b.Top < a.Bottom || b.Bottom > a.Top || b.Left > a.Right; } } sealed class LevelProcessor { readonly AutoBookmarkFilter[] _filters; readonly AutoBookmarkOptions.LevelAdjustmentOption[] _options; internal LevelProcessor(IList options) { var l = options.Count; _filters = new AutoBookmarkFilter[l]; _options = new AutoBookmarkOptions.LevelAdjustmentOption[l]; for (int i = 0; i < l; i++) { _filters[i] = options[i].Condition.CreateFilter(); _options[i] = options[i]; } } internal float ChangeSizeLevel(AutoBookmarkContext context) { for (int i = 0; i < _options.Length; i++) { var o = _options[i]; if (o.FilterBeforeMergeTitle && context.IsTextMerged) { continue; } if (_filters[i].Matches(context)) { return o.RelativeAdjustment ? o.AdjustmentLevel + context.TextInfo.Size : o.AdjustmentLevel; } } return context.TextInfo.Size; } } } }