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