fschutt / printpdf

An easy-to-use library for writing PDF in Rust
https://docs.rs/printpdf/
MIT License
802 stars 96 forks source link

feature request: bookmarks #85

Closed g-w1 closed 3 years ago

g-w1 commented 3 years ago

Hi,

Thanks for the awesome library. One feature that I would really like is bookmarks in the pdf that you can put on a page. This bookmark shows up on the left of the page in firefox and is accessible on the kindle very easily with the goto menu. (The kindle is my use case.)

I do not know how simple this would be to implement, but it doesn't seem that hard.

Thanks so much.

for reference, here is how the feature looks in firefox (on the left there is a list of pages) image

And if it helps, my use case is making a code renderer that can render source code for a project in a pdf file so it can be read on a kindle, so I would like it to be able to jump around quickly with bookmarks.

Edit:

Just saw this is in the TODOs. I could try to implement this if you could give me a little background, since I know very little about pdfs. I think there could be a function for a page called set_bookmark that just sets a bookmark on a page and accepts a &str for the name of the bookmark.

g-w1 commented 3 years ago

After some investigation I found this is how you do it:

509 0 obj
<<
/Parent 5 0 R
/Next 524 0 R
/Title (ir\056zig\056pdf)
/A 508 0 R
/Prev 505 0 R
>>

It seems like this is a doubly linked list with a prev and next (I think the object numbers, I dont really know anything about pdfs, this was just examining it with a text editor. /A is actually the page it is referring (marked) to.)

Hopefully this gives more insight.

fschutt commented 3 years ago

Yeah, it's probably not that hard to implement, but I've not done it since it wasn't important for me. If you want to implement it, you'd probably want to start like this: https://github.com/fschutt/printpdf/blob/1b0aa7af98a0f9b082a580463156c6a548226788/src/types/pdf_layer.rs#L124-L133

I.e. .add_bookmark(page: &PdfPageId, text: &str)

The only difficulty is that you need to create the bookmark after the page that you're referencing has been created, plus you'd need access to the internal PDF object ID. Maybe instead of writing the bookmarks directly, just store all bookmarks as a HashMap and defer writing them until the document is being saved - because you can't really reference the PDF page until the document is actually written. Otherwise you're going to have a problem trying to reference a page whose PDF object ID hasn't yet been determined.

You need to insert a Reference to reference a page, otherwise refer to the Adobe PDF spec, it probably has some info on creating bookmarks.

g-w1 commented 3 years ago

Thanks. Would the object I would want to use be Object::Dictionary(Dictionary) from LoPdf to implement this feature? You were right, the spec did have a feature for this spec pic.

Edit:

This feature is turning out harder than I thought. The main confusion point for me is where I insert the information. It does not seem like the font setting, because the font is a /Tf name size, while this requires a whole new object (Dictionary) like this (listing the first, last, and how many bookmarks)

5 0 obj
<<
/Count 49
/First 6 0 R
/Last 1559 0 R
>>
endobj

plus an individual object for each bookmark

6 0 obj
<<
/Parent 5 0 R
/Title (astgen\056zig\056pdf)
/A 4 0 R
/Next 73 0 R
>>
endobj

If you could catch me up to speed on where exactly and what functions I should use to insert these "objects" that would be awesome!

fschutt commented 3 years ago

Dictionaries can be built from a Vec<(String, Object)>, if you pass that to lopdf, it'll create a dictionary. See here:

https://github.com/fschutt/printpdf/blob/1b0aa7af98a0f9b082a580463156c6a548226788/src/types/pdf_document.rs#L327-L332

(I've also noticed that Use0 is technically not a valid value, should be UseNone instead). But basically you need to add a new ("Outlines", Reference(outline_dictionary)) to that /Catalog, after the PageMode

image

image

You need to "reserve" a new object ID for the catalog with document.inner_doc.new_object_id(), because it has to be an indirect reference, not a direct one. If you're not sure about how the final PDF should look like, one technique I like to do is to download PDFExplorer (see README) and open an existing PDF that already has working bookmarks, that way you know where to insert them.