TocFromOutlines.cs
//
// This code is part of Document Solutions for PDF demos.
// Copyright (c) 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
{
    // This example shows how to use the Outlines collection of an existing PDF
    // to build a table of contents and insert that TOC at the top of the document.
    public class TocFromOutlines
    {
        public int CreatePDF(Stream stream)
        {
            // TOC layout setup:
            var margin = 36;
            var levelOffset = 12;
            // Horizontal space allocated for the page numbers:
            var pageSpace = 24;
            // Vertical gap between TOC entries:
            var gap = 4;

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

            // Add sacrificial page and create text layout:
            var page = doc.Pages.Add();
            var tl = page.Graphics.CreateTextLayout();
            InitLayout(0);
            // Measure a dot:
            var dotW = page.Graphics.MeasureString(new string('.', 12), tl.DefaultFormat).Width / 12;

            // Dry run to count the number of pages in the TOC:
            float top = margin;
            int tocPages = 0;
            bool drawCaption = true;
            MakeToc(doc.Outlines, 0, true);

            // Live run to insert the TOC in the doc:
            doc.Pages.RemoveAt(doc.Pages.Count - 1);
            page = doc.Pages.Insert(0);
            InitLayout(0);
            top = margin;
            drawCaption = true;
            MakeToc(doc.Outlines, 0, false);

            // Done:
            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)
                        // NOTE: this loses the exact positioning on the target page, to fix create exact destination type copy:
                        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);
                    // Ignore destinations without a target page:
                    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)
                        {
                            // Draw outline text:
                            page.Graphics.DrawTextLayout(tl, PointF.Empty);
                            // Draw page number:
                            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);
                            // Draw dots:
                            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);
                            // Make link:
                            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));
                            // Debug: draw red border on converted page index destinations, blue on untouched originals:
                            // page.Graphics.DrawRectangle(rc, newDest != null ? Color.Red : Color.Blue);
                        }
                    }
                    MakeToc(node.Children, level + 1, dryRun);
                }
            }
        }
    }
}