public void CreatePDF(Stream stream) { var doc = new GcPdfDocument(); var font = StandardFonts.Times; var fontSize = 12; // 1/2" margins all around (72 dpi is the default resolution used by GcPdf): var margin = 72 / 2; var pageWidth = doc.PageSize.Width; var pageHeight = doc.PageSize.Height; var cW = pageWidth - margin * 2; // Text format for the chapter titles: var tlCaption = new TextLayout(72); tlCaption.DefaultFormat.Font = font; tlCaption.DefaultFormat.FontSize = fontSize + 4; tlCaption.DefaultFormat.Underline = true; tlCaption.MaxWidth = pageWidth; tlCaption.MaxHeight = pageHeight; tlCaption.MarginLeft = tlCaption.MarginTop = tlCaption.MarginRight = tlCaption.MarginBottom = margin; tlCaption.TextAlignment = TextAlignment.Center; // Height of chapter caption (use a const for simplicity): const float captionH = 24; // Text layout for main document body (default GcPdf resolution is 72dpi): var tl = new TextLayout(72); tl.DefaultFormat.Font = font; tl.DefaultFormat.FontSize = fontSize; tl.FirstLineIndent = 72 / 2; tl.MaxWidth = pageWidth; tl.MaxHeight = pageHeight; tl.MarginLeft = tl.MarginRight = tl.MarginBottom = margin; tl.MarginTop = margin + captionH; tl.ColumnWidth = cW * 0.3f; tl.TextAlignment = TextAlignment.Justified; tl.AlignmentDelayToSplit = true; // Array of PageSplitArea's which control additional columns (1st column is controlled by // the 'main' TextLayout, for each additional one a PageSplitArea must be provided - // it will create and return a TextLayout that can then be used to render the column): var psas = new PageSplitArea[] { new PageSplitArea(tl) { MarginLeft = tl.MarginLeft + (cW * 0.35f) }, new PageSplitArea(tl) { ColumnWidth = -cW * 0.3f } }; // Split options to control splitting text between pages: TextSplitOptions tso = new TextSplitOptions(tl) { RestMarginTop = margin, MinLinesInFirstParagraph = 2, MinLinesInLastParagraph = 2 }; // Generate a number of "chapters", provide outline entry for each: const int NChapters = 20; doc.Pages.Add(); for (int i = 0; i < NChapters; ++i) { // Print chapter header across all columns: string chapter = $"Chapter {i + 1}"; tlCaption.Clear(); tlCaption.Append(chapter); tlCaption.PerformLayout(true); doc.Pages.Last.Graphics.DrawTextLayout(tlCaption, PointF.Empty); // Add outline node for the chapter: doc.Outlines.Add(new OutlineNode(chapter, new DestinationFitV(doc.Pages.Count - 1, null))); // // Clear last chapter's text and add new chapter: tl.FirstLineIsStartOfParagraph = true; tl.LastLineIsEndOfParagraph = true; tl.Clear(); tl.Append(Common.Util.LoremIpsum(5, 7, 9, 15, 25)); tl.PerformLayout(true); // Variable to hold last chapter end's bottom coord: float contentBottom = 0f; // Print the chapter: var tls = new TextLayoutSplitter(tl); while (true) { var tlCol0 = tls.SplitAndBalance(psas, tso); var g = doc.Pages.Last.Graphics; g.DrawTextLayout(tlCol0, PointF.Empty); g.DrawTextLayout(psas[0].TextLayout, PointF.Empty); g.DrawTextLayout(psas[1].TextLayout, PointF.Empty); if (tls.SplitResult != SplitResult.Split) { // End of chapter, find out how much height left on page for next chapter: contentBottom = tl.ContentY + tl.ContentHeight; contentBottom = Math.Max(contentBottom, psas[0].TextLayout.ContentRectangle.Bottom); contentBottom = Math.Max(contentBottom, psas[1].TextLayout.ContentRectangle.Bottom); // Done printing chapter: break; } // Continue printing chapter on new page: psas[0].MarginTop = psas[1].MarginTop = margin; doc.Pages.Add(); } // Next chapter - find out if we have enough space left on current page to start new chapter: if (contentBottom + captionH < pageHeight * 0.8f) { // Start new chapter on current page: contentBottom += pageHeight * 0.05f; tlCaption.MarginTop = contentBottom; tl.MarginTop = psas[0].MarginTop = psas[1].MarginTop = contentBottom + captionH; } else if (i < NChapters - 1) { // Start new chapter on new page: tlCaption.MarginTop = margin; tl.MarginTop = psas[0].MarginTop = psas[1].MarginTop = margin + captionH; doc.Pages.Add(); } } // Done: doc.Save(stream); }
// Adds a word index to the end of the passed document: private void AddWordIndex(GcPdfDocument doc, IEnumerable <string> words) { var tStart = DateTime.Now; // Words and page indices where they occur, sorted on words: SortedDictionary <string, List <int> > index = new SortedDictionary <string, List <int> >(); // Here the main loop building the index is on key words. // An alternative would be to loop over the pages. // Depending on the relative sizes of the keyword dictionary vs // the number of pages in the document, one or the other might be better, // but this is beyond the scope of this sample. foreach (string word in words) { bool wholeWord = word.IndexOf(' ') == -1; var ft = doc.FindText(new FindTextParams(word, wholeWord, false), OutputRange.All); var pgs = ft.Select(fp_ => fp_.PageIndex).Distinct(); // A very simplistic way of also finding plurals: if (wholeWord && !word.EndsWith('s')) { var ftpl = doc.FindText(new FindTextParams(word + "s", wholeWord, false), OutputRange.All); pgs = pgs.Concat(ftpl.Select(fp_ => fp_.PageIndex).Distinct()).Distinct(); } if (pgs.Any()) { var sorted = pgs.ToList(); sorted.Sort(); index.Add(word, sorted); } } // Prepare to render the index. The whole index is built // in a single TextLayout instance, set up to render it // in two columns per page. // The main rendering loop uses the TextLayout.SplitAndBalance method // using the approach demonstrated in BalancedColumns sample. // The complication here is that we need to associate a link to the // relevant page with each page number rendered, see linkIndices below. // Set up the TextLayout: const float margin = 72; var pageWidth = doc.PageSize.Width; var pageHeight = doc.PageSize.Height; var cW = pageWidth - margin * 2; // Caption (index letter) format: var tfCap = new TextFormat() { FontName = _fontFamily, FontBold = true, FontSize = 16, LineGap = 24, }; // Index word and pages format: var tfRun = new TextFormat() { FontName = _fontFamily, FontSize = 10, }; // Page headers/footers: var tfHdr = new TextFormat() { FontName = _fontFamily, FontItalic = true, FontSize = 10, }; // FirstLineIndent = -18 sets up hanging indent: var tl = new TextLayout(72) { 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, }; // The list of text runs created for page numbers: List <Tuple <TextRun, int> > pgnumRuns = new List <Tuple <TextRun, int> >(); // This loop builds the index on the TextLayout, saving the text runs // created for each page number rendered. Note that at this point // (prior to the PerformLayout(true) call) the text runs do not contain any info // about their code points and render locations, so we can only save the text runs here. // Later they will be used to add links to referenced pages in the PDF: char litera = ' '; foreach (KeyValuePair <string, List <int> > kvp in index) { var word = kvp.Key; var pageIndices = kvp.Value; if (Char.ToUpper(word[0]) != litera) { litera = Char.ToUpper(word[0]); tl.Append($"{litera}\u2029", tfCap); } tl.Append(word, tfRun); tl.Append(" ", tfRun); for (int i = 0; i < pageIndices.Count; ++i) { var from = pageIndices[i]; var tr = tl.Append((from + 1).ToString(), tfRun); pgnumRuns.Add(Tuple.Create(tr, from)); // We merge sequential pages into "..-M": int k = i; for (int j = i + 1; j < pageIndices.Count && pageIndices[j] == pageIndices[j - 1] + 1; ++j) { k = j; } if (k > i + 1) { tl.Append("-", tfRun); var to = pageIndices[k]; tr = tl.Append((to + 1).ToString(), tfRun); pgnumRuns.Add(Tuple.Create(tr, to)); // Fast forward: i = k; } if (i < pageIndices.Count - 1) { tl.Append(", ", tfRun); } else { tl.AppendLine(tfRun); } } } // This calculates the glyphs and lays out the whole index. // The tl.SplitAndBalance() call in the loop below does not require redoing the layout: tl.PerformLayout(true); // // Now we are ready to split and render the text layout, and also add links to page numbers. // // Split areas and options - see BalancedColumns for details: var psas = new PageSplitArea[] { new PageSplitArea(tl) { MarginLeft = tl.MarginLeft + (cW * 0.54f) }, }; TextSplitOptions tso = new TextSplitOptions(tl) { KeepParagraphLinesTogether = true, }; // First original code point index in the current column: int cpiStart = 0; // Max+1 original code point index in the current column: int cpiEnd = 0; // Current index in pgnumRuns: int pgnumRunsIdx = 0; // Split and render the index in 2 columns: for (var page = doc.Pages.Add(); ; page = doc.Pages.Add()) { var g = page.Graphics; // Add a simple page header: g.DrawString($"Index generated by GcPdf on {tStart}", tfHdr, new RectangleF(margin, 0, pageWidth - margin * 2, margin), TextAlignment.Center, ParagraphAlignment.Center, false); // 'rest' will accept the text that did not fit on this page: var splitResult = tl.SplitAndBalance(psas, tso, out TextLayout rest); // Render text: g.DrawTextLayout(tl, PointF.Empty); g.DrawTextLayout(psas[0].TextLayout, PointF.Empty); // Add links from page numbers to pages: linkIndices(tl, page); linkIndices(psas[0].TextLayout, page); // Are we done yet? if (splitResult != SplitResult.Split) { break; } tl = rest; } // Done: return; // Method to add links to actual pages over page numbers in the current column: void linkIndices(TextLayout tl_, Page page_) { cpiEnd += tl_.CodePointCount; for (; pgnumRunsIdx < pgnumRuns.Count; ++pgnumRunsIdx) { var run = pgnumRuns[pgnumRunsIdx]; var textRun = run.Item1; int cpi = textRun.CodePointIndex; if (cpi >= cpiEnd) { break; } cpi -= cpiStart; var rects = tl_.GetTextRects(cpi, textRun.CodePointCount); System.Diagnostics.Debug.Assert(rects.Count > 0); page_.Annotations.Add(new LinkAnnotation(rects[0].ToRectangleF(), new DestinationFit(run.Item2))); } cpiStart += tl_.CodePointCount; } }