WordIndex.vb
'' 
'' このコードは、DioDocs for PDF のサンプルの一部として提供されています。
'' © MESCIUS inc. All rights reserved.
'' 
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 サンプルで示されている手法を使用して、2つの
'' バランスの取れた列に描画されます。
'' 
'' 注意:このサンプルをダウンロードし、有効な DsPdf ライセンスなしでローカルシステム上で
'' 実行すると、サンプル PDF の最初の5ページだけが読み込まれ、その5ページのインデックス
'' のみが生成されます。
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)
            '' PDF ドキュメントを保存します。
            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
        '' インスタンスに組み込まれ、1ページあたり2列で描画されるように設定されています。
        '' メインの描画ループでは、BalancedColumns サンプルで示された手法を使用して
        '' TextLayout.SplitAndBalance メソッドを使用します。
        '' ここで問題となるのは、関連するページへのリンクを描画した各ページ番号に関連付ける
        '' 必要があることです。以下の linkIndices を参照してください。
        '' 
        '' TextLayout を設定します。
        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.SplitAndBalanc() 呼び出しでは、レイアウトをやり直す必要はありません。
        tl.PerformLayout(True)

        '' 
        '' これで、テキストレイアウトを分割して描画し、ページ番号へのリンクを追加する準備が整いました。
        '' 

        '' 分割された領域とオプション - 詳細は 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

        '' インデックスを2列に分割して描画します。
        Dim page = doc.Pages.Add()
        While True
            Dim g = Page.Graphics
            '' シンプルなページヘッダを追加します。
            g.DrawString($"Index generated by DsPdf on {tStart}", 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
        '' PDF ドキュメントを保存します。
    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