Knoxville, TN

Adding bookmarks to PDFs with .NET

September 12, 2024

Over the last year or so, I’ve been working up some of my TTRPG adventure notes into PDFs that I’ve released on DMs Guild, DriveThruRPG, and itch.io.

Homebrewery is a pretty nice entry-level approach to this (as I wrote in my previous post), but you’re at the mercy of your browser and your print-to-PDF driver. There are some obvious but hard-to-fix issues like file size (my best shot at this was reducing the size of images and running the result through FoxIt’s free PDF compressor), but what’s less obvious is a lot of features you just don’t get without going through professional PDF authoring software.

One of those features that you just can’t get from print-to-PDF is bookmarks. If you’ve ever used a PDF version of a book as a reference, you know you’re flipping around a lot, and Ctrl-F or guessing page numbers or mindlessly scrolling is annoying. The longer the document, the more you need that handy menu on the sidebar.

Turns out, it’s pretty easy to create a LINQpad script using the free .NET library PDFSharp. (LINQPad is extremely handy for writing up little .NET scripts like you’d do in scripting languages or languages with interactive shells, plus it lets you visualize complex objects pretty easily when you’re prototyping.)

I just create a script that I run every time I create a new version, hard-coding a table of contents that I apply using PDFSharp’s Outlines collection. I also clean up the title, which is generated automatically from the page that gets printed.

void Main()
{
	var FILENAME = Path.Combine(Path.GetDirectoryName(Util.CurrentQueryPath), "Ryuutama - A Mysterious Tune.pdf");

	var BOOKMARKS = new TOCEntry[] {
		new TOCEntry() { Name = "Plot", Page = 1 },
		new TOCEntry() { Name = "GM Overview", Page = 1, Children = new TOCEntry[] {
			new TOCEntry() { Name = "Setting", Page = 1 },
			new TOCEntry() { Name = "Characters", Page = 1 },
			new TOCEntry() { Name = "Monsters", Page = 1 },
			new TOCEntry() { Name = "Plot Resolution", Page = 1 }
		}},
		new TOCEntry() { Name = "Scenes", Page = 2, Children = new TOCEntry[] {
			new TOCEntry() { Name = "Entering The Haile", Page = 2 },
			new TOCEntry() { Name = "Taking the job", Page = 2 },
			new TOCEntry() { Name = "Setting out on the trail", Page = 3 },
			new TOCEntry() { Name = "Trail to the Keyhole", Page = 3 },
			new TOCEntry() { Name = "Camp on the ridge", Page = 4 },
			new TOCEntry() { Name = "The valley", Page = 4 },
			new TOCEntry() { Name = "The Greenhouse", Page = 5, Children = new TOCEntry[] {
				new TOCEntry() { Name = "Exterior", Page = 5 },
				new TOCEntry() { Name = "Top Floor", Page = 5 },
				new TOCEntry() { Name = "Basement", Page = 6 },
				new TOCEntry() { Name = "Cave", Page = 6 }
			}}
		}},
		new TOCEntry() { Name = "Credits", Page = 8 }
	};
	
	using (var pdf = PdfReader.Open(FILENAME))
	{
		pdf.Info.Title = "A Mysterious Tune - Ryuutama Adventure";
		pdf.Outlines.Clear();
		BuildTocs(BOOKMARKS, pdf);
		pdf.Save(FILENAME);
	}
}

void BuildTocs(TOCEntry[] tocs, PdfDocument pdf, PdfOutline parent = null)
{
	foreach (var toc in tocs) BuildToc(toc, pdf, parent);
}

void BuildToc(TOCEntry toc, PdfDocument pdf, PdfOutline parent = null)
{
	var bookmark = (parent != null ? parent.Outlines : pdf.Outlines).Add(toc.Name, pdf.Pages[toc.Page], true);
	if (toc.Children != null) BuildTocs(toc.Children, pdf, bookmark);
}

class TOCEntry
{
	public string Name;
	public int Page;
	public TOCEntry[] Children;
}

×