WordIndex.vb
'' 完毕:
Imports System.IO
Imports System.Drawing
Imports GrapeCity.Documents.Pdf
Imports GrapeCity.Documents.Pdf.TextMap
Imports GrapeCity.Documents.Text
Imports GrapeCity.Documents.Common
Imports GrapeCity.Documents.Pdf.Annotations

'' 此示例加载现有的 PDF,并使用预定义的关键字列表,
'' 建立链接到它们出现的页面的那些单词的字母索引
'' 在文件中。生成的索引页附加到原始文档中,
'' 并保存在新的 PDF 中。
'' 使用以下技术将索引呈现在两个平衡列中
'' 在BalancedColumns示例中进行了演示。
'' 
'' 注意:如果您下载此示例并在您自己的系统上本地运行它
'' 没有有效的 GcDocs.Pdf 许可证,只有示例 PDF 的前五页
'' 将被加载,并且只会为这五个页面生成索引。
Public Class WordIndex

    '' 字体集合来保存我们需要的字体:
    Private _fc As FontCollection = New FontCollection()
    '' 本示例中使用的字体系列(不区分大小写):
    Const _fontFamily = "segoe ui"

    '' 主要样本条目:
    Function CreatePDF(ByVal stream As Stream) As Integer
        '' 使用我们需要的字体设置字体集合:
        _fc.RegisterDirectory(Path.Combine("Resources", "Fonts"))

        '' 获取要添加索引的 PDF:
        Dim tfile = Path.Combine("Resources", "PDFs", "CompleteJavaScriptBook.pdf")

        '' 我们将在其上构建索引的单词列表:
        Dim words = _keywords.Distinct(StringComparer.InvariantCultureIgnoreCase).Where(Function(w_) Not String.IsNullOrEmpty(w_))

        '' 加载 PDF 并添加索引:
        Using fs = New FileStream(tfile, FileMode.Open, FileAccess.Read)
            Dim doc = New GcPdfDocument()
            doc.Load(fs)
            ''
            Dim origPageCount = doc.Pages.Count
            '' 构建并添加索引:
            AddWordIndex(doc, words)
            '' 默认在第一个索引页打开文档
            '' (可能无法在浏览器查看器中工作,但可以在 Acrobat 中工作):
            doc.OpenAction = New DestinationFit(origPageCount)
            '' 完毕:
            doc.Save(stream)
            Return doc.Pages.Count
        End Using
    End Function

    '' 用于构建索引的单词列表:
    Private ReadOnly _keywords() As String =
        {
            "JavaScript", "Framework", "MVC", "npm", "URL", "CDN", "HTML5", "CSS", "ES2015", "web",
            "Node.js", "API", "model", "view", "controller", "data management", "UI", "HTML",
            "API", "function", "var", "component", "design pattern", "React.js", "Angular", "AJAX",
            "DOM", "TypeScript", "ECMAScript", "CLI", "Wijmo", "CoffeeScript", "Elm",
            "plugin", "VueJS", "Knockout", "event", "AngularJS", "pure JS", "data binding", "OOP", "GrapeCity",
            "gauge", "JSX", "mobile", "desktop", "Vue", "template", "server-side", "client-side",
            "SPEC", "RAM", "ECMA"
        }

    '' 在文档或页面上调用 FindText() 会动态为每个页面构建文本映射。
    '' 重用缓存的文本地图可以大大加快速度。
    Private Function FindTextPages(ByVal maps As ITextMap(), ByVal tp As FindTextParams) As SortedSet(Of Integer)
        Dim finds = New SortedSet(Of Integer)
        Dim currPageIdx = -1
        For Each map In maps
            currPageIdx = map.Page.Index
            map.FindText(tp, Function(fp_) finds.Add(currPageIdx))
        Next
        Return finds
    End Function

    '' 将单词索引添加到传递的文档的末尾:
    Private Sub AddWordIndex(ByVal doc As GcPdfDocument, ByVal words As IEnumerable(Of String))
        Dim tStart = Util.TimeNow()

        '' 为所有页面构建文本映射以加速 FindText() 调用:
        Dim textMaps(doc.Pages.Count - 1) As ITextMap
        For i = 0 To doc.Pages.Count - 1
            textMaps(i) = doc.Pages(i).GetTextMap()
        Next

        '' 单词和出现的页面索引,按单词排序:
        Dim index = New SortedDictionary(Of String, List(Of Integer))()

        '' 这里构建索引的主循环是针对关键词的。
        '' 另一种方法是循环页面。
        '' 取决于关键字词典与关键字词典的相对大小
        '' 文档的页数,其中之一可能更好,
        '' 但这超出了本示例的范围。
        For Each word In words
            Dim wholeWord As Boolean = word.IndexOf(" "c) = -1
            Dim pgs = FindTextPages(textMaps, New FindTextParams(word, wholeWord, False))
            '' 查找复数的一种非常简单的方法:
            If wholeWord AndAlso Not word.EndsWith("s") Then
                pgs.UnionWith(FindTextPages(textMaps, New FindTextParams(word + "s", wholeWord, False)))
            End If
            If (pgs.Any()) Then
                index.Add(word, pgs.ToList())
            End If
        Next

        '' 准备渲染索引。整个索引建立完毕
        '' 在单个 TextLayout 实例中,设置为渲染它
        '' 每页两栏。
        '' 主渲染循环使用 TextLayout.SplitAndBalance 方法
        '' 使用BalancedColumns示例中演示的方法。
        '' 这里的复杂之处在于我们需要将链接关联到
        '' 相关页面及其呈现的每个页码,请参阅下面的链接索引。
        '' 设置文本布局:
        Const margin = 72.0F
        Dim pageWidth = doc.PageSize.Width
        Dim pageHeight = doc.PageSize.Height
        Dim cW = pageWidth - margin * 2
        '' 标题(索引字母)格式:
        Dim tfCap = New TextFormat() With {
            .FontName = _fontFamily,
            .FontBold = True,
            .FontSize = 16,
            .LineGap = 24
        }
        '' 索引字及页码格式:
        Dim tfRun = New TextFormat() With {
            .FontName = _fontFamily,
            .FontSize = 10
        }
        '' 页眉/页脚:
        Dim tfHdr = New TextFormat() With {
            .FontName = _fontFamily,
            .FontItalic = True,
            .FontSize = 10
        }
        '' FirstLineIndent = -18 设置悬挂缩进:
        Dim tl = New TextLayout(72) With {
            .FontCollection = _fc,
            .FirstLineIndent = -18,
            .MaxWidth = pageWidth,
            .MaxHeight = pageHeight,
            .MarginLeft = margin,
            .MarginRight = margin,
            .MarginBottom = margin,
            .MarginTop = margin,
            .ColumnWidth = cW * 0.46F,
            .TextAlignment = TextAlignment.Leading,
            .ParagraphSpacing = 4,
            .LineGapBeforeFirstLine = False
        }

        '' 为页码创建的文本运行列表:
        Dim pgnumRuns = New List(Of Tuple(Of TextRun, Integer))()
        '' 此循环在 TextLayout 上构建索引,保存文本运行
        '' 为呈现的每个页码创建。请注意,此时
        '' (在 PerformLayout(true) 调用之前)文本运行不包含任何信息
        '' 关于它们的代码点和渲染位置,因此我们只能在此处保存文本运行。
        '' 稍后它们将用于添加指向 PDF 中引用页面的链接:
        Dim litera As Char = " "
        For Each kvp In index
            Dim word = kvp.Key
            Dim pageIndices = kvp.Value
            If Char.ToUpper(word(0)) <> litera Then
                litera = Char.ToUpper(word(0))
                tl.Append($"{litera}{ChrW(&H2029)}", tfCap)
            End If
            tl.Append(word, tfRun)
            tl.Append("  ", tfRun)
            For i = 0 To pageIndices.Count - 1
                Dim from_ = pageIndices(i)
                Dim tr = tl.Append((from_ + 1).ToString(), tfRun)
                pgnumRuns.Add(Tuple.Create(Of TextRun, Integer)(tr, from_))
                '' 我们将连续的页面合并为“..-M”:
                Dim k = i
                For j = i + 1 To pageIndices.Count - 1
                    If pageIndices(j) <> pageIndices(j - 1) + 1 Then
                        Exit For
                    End If
                    k = j
                Next
                If (k > i + 1) Then
                    tl.Append("-", tfRun)
                    Dim to_ = pageIndices(k)
                    tr = tl.Append((to_ + 1).ToString(), tfRun)
                    pgnumRuns.Add(Tuple.Create(Of TextRun, Integer)(tr, to_))
                    '' 快进:
                    i = k
                End If
                If (i < pageIndices.Count - 1) Then
                    tl.Append(", ", tfRun)
                Else
                    tl.AppendLine(tfRun)
                End If
            Next
        Next
        '' 这将计算字形并布置整个索引。
        '' 下面循环中的 tl.SplitAndBalance() 调用不需要重做布局:
        tl.PerformLayout(True)

        '' 现在我们准备分割和渲染文本布局,并添加页码的链接。

        '' Split 区域和选项 - 有关详细信息,请参阅BalancedColumns:
        Dim psas() As PageSplitArea = {
            New PageSplitArea(tl) With {.MarginLeft = tl.MarginLeft + (cW * 0.54F)}
        }
        Dim tso = New TextSplitOptions(tl) With {
            .KeepParagraphLinesTogether = True
        }

        '' 当前列中的第一个原始代码点索引:
        Dim cpiStart = 0
        '' 当前列中的 Max+1 原始码点索引:
        Dim cpiEnd = 0
        '' pgnumRuns 中的当前索引:
        Dim pgnumRunsIdx = 0

        '' 在当前列的页码上添加指向实际页面的链接的方法:
        Dim linkIndices As Action(Of TextLayout, Page) =
            Sub(tl_, page_)
                cpiEnd += tl_.CodePointCount
                While pgnumRunsIdx < pgnumRuns.Count
                    Dim run = pgnumRuns(pgnumRunsIdx)
                    Dim textRun = run.Item1
                    Dim cpi = textRun.CodePointIndex
                    If cpi >= cpiEnd Then
                        Exit While
                    End If
                    cpi -= cpiStart
                    Dim rects = tl_.GetTextRects(cpi, textRun.CodePointCount)
                    Debug.Assert(rects.Count > 0)
                    page_.Annotations.Add(New LinkAnnotation(rects(0).ToRectangleF(), New DestinationFit(run.Item2)))
                    pgnumRunsIdx += 1
                End While
                cpiStart += tl_.CodePointCount
            End Sub

        '' 将索引拆分并呈现为两列:
        Dim page = doc.Pages.Add()
        While True
            Dim g = Page.Graphics
            '' 添加一个简单的页眉:
            g.DrawString($"Index generated by DsPdf on {tStart:R}", tfHdr,
                New RectangleF(margin, 0, pageWidth - margin * 2, margin),
                TextAlignment.Center, ParagraphAlignment.Center, False)
            '' 'rest' 将接受不适合此页面的文本:
            Dim rest As TextLayout = Nothing
            Dim splitResult = tl.SplitAndBalance(psas, tso, rest)
            '' 渲染文本:
            g.DrawTextLayout(tl, PointF.Empty)
            g.DrawTextLayout(psas(0).TextLayout, PointF.Empty)
            '' 添加从页码到页面的链接:
            linkIndices(tl, page)
            linkIndices(psas(0).TextLayout, page)
            '' 我们完成了吗?
            If splitResult <> SplitResult.Split Then
                Exit While
            End If
            tl = rest
            page = doc.Pages.Add()
        End While
        '' 完毕:
    End Sub

    '' 创建一个包含 100 页“lorem ipsum”的示例文档:
    Private Function MakeDocumentToIndex() As String
        Const N = 100
        Dim tfile = Path.GetTempFileName()
        Using fsOut = New FileStream(tfile, FileMode.Open, FileAccess.ReadWrite)
            Dim tdoc = New GcPdfDocument()
            '' 有关StartDoc/EndDoc模式的详细信息请参见StartEndDoc:
            tdoc.StartDoc(fsOut)
            '' 准备一个 TextLayout 来保存/格式化文本:
            Dim tl = New TextLayout(72)
            tl.FontCollection = _fc
            tl.DefaultFormat.FontName = _fontFamily
            tl.DefaultFormat.FontSize = 12
            '' 使用 TextLayout 布局整个页面,包括边距:
            tl.MaxHeight = tdoc.PageSize.Height
            tl.MaxWidth = tdoc.PageSize.Width
            tl.MarginAll = 72
            tl.FirstLineIndent = 72 / 2
            '' 生成文档:
            For pageIdx = 0 To N - 1
                tl.Append(Util.LoremIpsum(1))
                tl.PerformLayout(True)
                tdoc.NewPage().Graphics.DrawTextLayout(tl, PointF.Empty)
                tl.Clear()
            Next
            tdoc.EndDoc()
        End Using
        Return tfile
    End Function
End Class