TocFromOutlines.cs
// 
// このコードは、DioDocs for PDF のサンプルの一部として提供されています。
// © MESCIUS inc. All rights reserved.
// 
using System;
using System.IO;
using System.Drawing;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Text;
using GrapeCity.Documents.Pdf.Annotations;

namespace DsPdfWeb.Demos
{
    // このサンプルでは、既存の PDF のアウトラインコレクションを使用して目次を作成し、
    // その目次をドキュメントの先頭に挿入する方法を紹介しています。
    public class TocFromOutlines
    {
        public int CreatePDF(Stream stream)
        {
            // 目次のレイアウト設定です。
            var margin = 36;
            var levelOffset = 12;
            // ページ番号の横の空白を設定します。
            var pageSpace = 24;
            // 目次項目の行間を設定します。
            var gap = 4;

            var doc = new GcPdfDocument();
            using var fs = File.OpenRead(Path.Combine("Resources", "PDFs", "guide-wetland-birds.pdf"));
            doc.Load(fs);

            // 計算用のページを追加し、テキストレイアウトを作成します。
            var page = doc.Pages.Add();
            var tl = page.Graphics.CreateTextLayout();
            InitLayout(0);
            // ドットの幅を計算します。
            var dotW = page.Graphics.MeasureString(new string('.', 12), tl.DefaultFormat).Width / 12;

            // 目次のページ数をカウントします。
            float top = margin;
            int tocPages = 0;
            bool drawCaption = true;
            MakeToc(doc.Outlines, 0, true);

            // ドキュメントに目次を挿入します。
            doc.Pages.RemoveAt(doc.Pages.Count - 1);
            page = doc.Pages.Insert(0);
            InitLayout(0);
            top = margin;
            drawCaption = true;
            MakeToc(doc.Outlines, 0, false);

            doc.Save(stream);
            return doc.Pages.Count;

            void InitLayout(int level)
            {
                tl.MarginTop = margin;
                tl.MarginBottom = margin;
                tl.MarginLeft = margin + levelOffset * level;
                tl.MarginRight = margin + pageSpace;
                tl.MaxWidth = page.Size.Width;
                tl.MaxHeight = page.Size.Height;
            }

            (int pageIdx, Destination newDest) PageIdxFromDest(DestinationBase dest)
            {
                IDestination dd;
                if (dest is DestinationRef df)
                    doc.NamedDestinations.TryGetValue(df.Name, out dd);
                else
                    dd = dest as Destination;
                if (dd != null)
                {
                    if (dd.Page != null)
                        return (doc.Pages.IndexOf(dd.Page) + tocPages + 1, null);
                    else if (dd.PageIndex.HasValue)
                        // 注:この場合、対象ページでの正確な位置が失われるため、正確な遷移先のコピーを作成するよう修正します。
                        return (dd.PageIndex.Value + tocPages + 1, new DestinationFit(dd.PageIndex.Value + tocPages + 1));
                }
                return (-1, null);
            }

            void MakeToc(OutlineNodeCollection nodes, int level, bool dryRun)
            {
                foreach (var node in nodes)
                {
                    var (pageIdx, newDest) = PageIdxFromDest(node.Dest);
                    // 対象ページがない遷移先は無視します。
                    if (pageIdx >= 0)
                    {
                        top = tl.MarginTop + tl.ContentHeight + gap;
                        if (drawCaption)
                        {
                            if (!dryRun)
                                page.Graphics.DrawString("Table of Contents", tl.DefaultFormat, new PointF(margin, margin));
                            top += 24;
                            drawCaption = false;
                        }
                        tl.Clear();
                        tl.MarginLeft = margin + levelOffset * level;
                        tl.MarginTop = top;
                        var run = tl.Append(node.Title);
                        tl.AppendParagraphBreak();
                        tl.PerformLayout(true);
                        if (!tl.ContentHeightFitsInBounds)
                        {
                            if (dryRun)
                                ++tocPages;
                            else
                                page = doc.Pages.Insert(doc.Pages.IndexOf(page) + 1);
                            InitLayout(level);
                            top = tl.MarginTop;
                            tl.PerformLayout(true);
                        }
                        if (!dryRun)
                        {
                            // アウトラインテキストを描画します。
                            page.Graphics.DrawTextLayout(tl, PointF.Empty);
                            // ページ番号を描画します。
                            var pageNo = (pageIdx + 1).ToString();
                            var pageW = page.Graphics.MeasureString(pageNo, tl.DefaultFormat).Width;
                            var trcs = tl.GetTextRects(run.CodePointIndex, run.CodePointCount, true, true);
                            var trc = trcs[trcs.Count - 1];
                            var rc = new RectangleF(0, trc.Top, page.Size.Width - margin, trc.Height);
                            page.Graphics.DrawString(pageNo, tl.DefaultFormat, rc, TextAlignment.Trailing, ParagraphAlignment.Near, false);
                            // ドットを描画します。
                            rc.X = trc.Right;
                            rc.Width = page.Size.Width - trc.Right - margin - pageW - dotW;
                            var dots = new string('.', (int)(rc.Width / dotW) - 1);
                            page.Graphics.DrawString(dots, tl.DefaultFormat, rc, TextAlignment.Trailing, ParagraphAlignment.Near, false);
                            // リンクを作成します。
                            rc = new RectangleF(tl.MarginLeft, tl.MarginTop, page.Size.Width - tl.MarginLeft - margin, trc.Bottom - trcs[0].Top);
                            page.Annotations.Add(new LinkAnnotation(rc, newDest ?? node.Dest));
                            // デバッグ:変換されたページインデックスには赤枠を描画し、そのままのものには青枠を描画します。
                            // page.Graphics.DrawRectangle(rc, newDest != null ? Color.Red : Color.Blue);
                        }
                    }
                    MakeToc(node.Children, level + 1, dryRun);
                }
            }
        }
    }
}